用 caddy 代替 nginx, 一行配置搞定 php-fpm 反向代理
缘由
caddy 其实并不是一个新项目, 虽然早在 caddy 1.x 版本的时候老灯就关注这个项目了, 但是,老灯其实最近才开始去了解它和尝试使用它.
为什么之前一直没用 caddy 呢? 一是当时 nginx 还是非常坚挺, 可以说在 nginx + php-fpm 这一搭配方面, 基本上无敌手. 二是, 当时我试用过 Caddyfile 后,发现它太简洁了,简洁到我以配置 nginx 的思维, 完全无法适应配置 Caddyfile. 没错, 我当时觉得 , 配置 nginx 比 Caddyfile 简单多了 (因为 nginx 配置已经熟练多年).
从提交记录看, https://github.com/caddyserver/caddy/blob/v1/LICENSE.txt 最早可以追溯到2015年.
简单来说, Caddy 是一个使用 Golang 编写的 http 静态文件服务器和 反向代理服务器.
大概一年前,作者重构了 caddy, 也就是现在的 caddy 2.x 版. HN 上面依然可以找到他宣布 caddy 2 出来的消息:
mholt on May 4, 2020 [–]
Hi HN -- this is what I've been working on for the last 14 months, with the help of many contributors and the backing of several sponsors. (Thank You!) Caddy 2 is a fresh new server experience. Some things might take getting used to, like having every site served over HTTPS unless you specify http:// explicitly in your config. But in general, it will feel familiar to v1 in a lot of ways. If you've used v1 before, I recommend going into v2 with a clean-slate using our Getting Started guide: https://caddyserver.com/docs/getting-started
I'm excited to finally get this out there. Let me know what questions you have!
via https://news.ycombinator.com/item?id=23070567
那么, 为什么现在对 Caddy 感兴趣了?
一. 云原生
-
单文件静态链接. 我在打包 PHP 应用的时候, 如果用 caddy, copy 一个文件即可. 不需要考虑各种动态链接和依赖. -
自带 metrics ( https://caddyserver.com/docs/metrics) -
自带 REST api (https://caddyserver.com/docs/api)
二. 作为 Golang 程序员, 对于基于 Go 实现的东西, 有一种天生的好感.
Caddyfile 结构
下图取自官方文档: https://caddyserver.com/docs/caddyfile/concepts#structure
整个配置文件按块划分.
第一个红色的大 block 是全局配置块, 如果存在,它必须放在第一个块的位置.
每个 option 的配置, 基本上是 key - value 或者 key - list 的形式.
每个 snippet 也是单独成为一个 block, snippet 可以方便相同的配置复用,避免手动 copy 配置使得同样的配置分布在多处而难以维护.
每个站点单独成为一个 block.
这个配置足够简洁, 对于新手也极其友好.
关于配置这一块, 想要进一步了解的,可以查看官方文档, 同时推荐读一下 飞雪无情写的 Caddy实战(十一)| Caddyfile 设计之美
配置 php-fpm 反向代理
从官方文档点击 "Common Patterns", 然后其中就有关于 PHP 的说明.
配置一个 caddy + php-fpm 的博客, 全部指令如下:
example.com
root * /var/www
php_fastcgi /blog/* localhost:9000
file_server
example.com
指定域名(默认监听https
),
root * /var/www
指定站点根目录.
file_server
用于serve静态文件.
404问题
没错, 上面的配置, 在大部分情况下是工作的. 对于现在一些基于流行 PHP 框架的应用, 比如 基于 Symfony / Laravel / CodeIgniter / CakePHP / Phalcon 等, 大多数是以单一入口来跑的. 对于单一入口的应用, 404 错误都是交给应用程序层面来处理的, 而不是 http 服务器.
对于非单一入口应用程序, 典型的是, 你手里有一个几年前的程序, 没有用任何框架, 这个时候, caddy 的默认配置, 404 的问题就出来了.
the problem
这个问题直接描述比较抽象, 所以我这里举例子说明. 假设配置如下:
# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
root * /app/public
# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
php_fastcgi 127.0.0.1:9000
file_server
handle_errors {
root * /etc/caddy/error
rewrite * /error.html
templates
file_server
}
}
本意是, 如果有 404 或 403 等错误, 由 /error.html
来处理, 但是实际上访问不存在的 url, 如
/this-is-not-found.html
/blah-this-is-another-notfound/xxxxxx
/xxxxxxx
/sub/xxx/sub/xxxxx
404 错误并没有发送给 handle_errors
这个 handler 来处理, 而是直接被 php index.php 处理了. 由于这个程序并不是单一入口应用, 因此它的 index.php 完全不知道如何错误 404 错误请求的 uri.
尝试解决
然后我继续查看 Caddy 文档, 发现 php_fastcgi
有一个 Expanded form , 我尝试把其中的 try_files {path} {path}/index.php index.php
修改为 try_files {path} {path}/index.php
之后,大部分 404 错误解决了. 但是以 .php 结尾的文件的 404 错误还是无法处理.
经过修改的完整的配置如下:
# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
root * /app/public
# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
# php_fastcgi 127.0.0.1:9000
route {
# Add trailing slash for directory requests
@canonicalPath {
file {path}/index.php
not path */
}
redir @canonicalPath {path}/ 308
# If the requested file does not exist, try index files
@indexFiles file {
# try_files {path} {path}/index.php index.php
try_files {path} {path}/index.php
split_path .php
}
rewrite @indexFiles {http.matchers.file.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles path *.php
reverse_proxy @phpFiles 127.0.0.1:9000 {
transport fastcgi {
split .php
}
}
}
file_server
handle_errors {
root * /etc/caddy/error
rewrite * /error.html
templates
file_server
}
}
目前这个配置还存在的问题, 无法处理 .php 结尾文件的 404 错误, 如
/this-is-no-fount.php
/sub1/sub2/sub3/xxxxx.php
这种并不存在的 php 文件, 还是会被代理到 php-fpm 进程处理, 然后触发 php-fpm 响应 " No input file specified. " 的错误, 这个错误并不友好, 也不是我们期望的处理方式.
为了避免 " No input file specified. " 错误, 在 nginx 里面我们一般只需要这样处理:
# Pass the php scripts to fastcgi server specified in upstream declaration.
location ~ \.php(/|$) {
# avoid No input file specified
try_files $uri =404;
include fastcgi_params;
# the missing param in fastcgi_params copied from fastcgi.conf
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
fastcgi_buffer_size 64k;
fastcgi_buffers 8 256k;
fastcgi_busy_buffers_size 256k;
}
没错, 神奇的地方就在于 try_files $uri =404;
这一行配置.
由于我其实等于是刚看了5分钟文档, 之前并没有真正使用 caddy, 因此我也不知道如何处理这一情况. 于是我把这个问题发到 github 上面了.
然后其中一个热心开发者 Francis Lavoie 马上给出了解决方案:
Francis Lavoie 觉得这是一个好问题. 他建议用一个 error 指令来解决:
# Trigger error for non-existing PHP scripts
@phpNotFound {
path *.php
not file
}
error @phpNotFound 404
这样, 不存在的 php 文件就会触发 404 错误, 不会继续交给 php-fpm 处理.
更好的实现方案
使用 error 指令 加 Expanded form 确实能解决问题. 但是这是一个典型的应用场景, 应该还可以使配置更简化.
由于我对于 caddy 的代码并不了解,我的想法是"解决问题", 所以我的理解是,简单地增加两个选项:
-
always_proxy_to_index (
try_files {path} {path}/index.php index.php
, off 的时候去掉最后面的index.php
) -
pass_to_error_handler (遇到404等错误时,抛给其它 handler, 如
handle_errors
)
但是, Francis Lavoie 的理解显然更加透彻, 他认为增加两个狭义的选项(使用场景非常有限), 容易使配置越来越复杂(后面要的功能更多,情况越复杂,这样的选项会越来越多). 于是便 有了这两个 PR
Add support for triggering errors from try_files
#4346 https://github.com/caddyserver/caddy/pull/4346
Implement try_files
override in Caddyfile directive #4347 https://github.com/caddyserver/caddy/pull/4347
大约一周过去了, 这两个 PR 都被 mholt (caddy 创始人和主要开发者) review 并合并进去了.
第一个 PR 其实就是给 try_files
实现了 类似 nginx =404
这种功能.
第二个 PR 是允许对 try_files
进行 override. 这样 php_fastcgi
默认的try_files
就可以被自定义了.
这两个提交预计会在 caddy 2.5.0 版本发布.
到时候, 我的配置就可以简写为:
# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
root * /app/public
# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
php_fastcgi localhost:9000 {
try_files {path} {path}/index.php =404
}
file_server
handle_errors {
root * /etc/caddy/error
rewrite * /error.html
templates
file_server
}
}
不得不说, 这样实现, 使得整个配置变得更优雅. 功能增加了, 但是并没有增加新的配置指令和选项.
思考
-
使用开源产品时,遇到了问题,不要抱怨, 要有耐心,积极参与讨论.
如果一个东西不完善, 并且你发现了这一点, 积极地提出来, 说不定开发者就采纳了, 改进了. 不单是你, 其它有同样需求的人也会受益.
-
实现一个东西的时候, 不要只考虑解决当前问题而导致代码膨胀, 而是要看到问题的本质, 以更优雅地方式去解决.
最后, 提一下, caddy 与 php 集成的时候, 还有一些性能优化相关的问题没解决, https://github.com/caddyserver/caddy/issues/3803