Caddy
Caddy 是一個go編寫的輕量配置化web server。
類似於nginx。有豐富的插件,配置也很簡單,自定義插件也很容易。更人性化。
官網上是這麼介紹的:Caddy is the HTTP/2 web server with automatic HTTPS.
(說實話官網v1版本的介紹並不怎麼清楚,反而是v2版本的介紹更明確)
Caddy官方文檔: https://caddyserver.com/v1/tutorial
GitHub地址: https://github.com/caddyserver/caddy
Caddy的功能
TLS證書的續訂 : TLS certificate renewal。
Caddy可以通過ACME協議(Let’s Encrypt 爲了實現自動化證書管理,制訂了 ACME 協議)和Let’s Encrypt(一個免費、開放、自動化的數字證書認證機構)進行證書的簽發、續訂等。這也是官方介紹的automatic HTTPS.
(這個功能嘗試了一次因爲網絡timeout,就沒有繼續研究。等有需求的時候,再嘗試吧。)
OCSP裝訂: OCSP Staplin。
在線證書狀態協議(Online Certificate Status )Protocol),簡稱 OCSP,是一個用於獲取 X.509 數字證書撤銷狀態的網際協議。 Web 服務端將主動獲取 OCSP 查詢結果,並隨證書一起發送給客戶端,以此讓客戶端跳過自己去尋求驗證的過程,提高 TLS 握手效率。
靜態文件服務器: static file serving。
這個可以用來進行文件管理、文件上傳、基於 MarkDown 的博客系統等等。只需簡單的配置。附鏈接:Caddy服務器搭建和實現文件共享
(這個值得嘗試,之後打算用caddy搭建一個MarkDown的博客玩玩)
反向代理
這個和nginx的功能一樣。什麼叫反向代理,什麼叫正向代理,請看我的這篇博客:反向代理和正向代理
Kubernetes入口 : Kubernetes Ingress.
k8s集羣的網絡入口。這個是要和traefik搶工作啊。(還是習慣使用treafik,不打算嘗試caddy)。
自定義中間件
這個可nginx可以寫lua插件一樣,caddy也支持寫插件。是用golang寫,得益於caddy代碼結構的組織,在caddy源碼基礎上擴展很容易。(TODO: 下一篇文章寫怎麼給caddy添加插件)
自定義服務器(ServerType)
Caddy本身是一個http服務器(const serverType = "http"
) ,但是通過擴展ServerType可以變成 SSH、SFTP、TCP等等,教科書一樣的典範是DNS服務器CoreDNS
Caddy代碼目錄
下面是caddy項目源碼的目錄,去除了相關文檔文件、test文件等。
directives 指令: 比如log、limits、proxy都是指令。
.
├── access.log // 訪問日誌
├── assets.go // 工具方法,用來獲取環境變量CADDYPATH和用戶目錄的路徑
├── caddy
│ ├── caddymain
│ │ ├── run.go // caddy服務啓動入口,解析參數、日誌輸出、讀取Caddyfile\設置cpu等等。
│ ├── main.go // 程序入口, main函數
├── caddy.go // 定義了caddy服務器相關概念和接口
├── caddyfile // caddy的配置文件caddyfile的解析和使用
│ ├── dispenser.go // 定義了一系列方便使用配置裏Token的方法,如Next、NextBlock、NextLine等
│ ├── json.go // caddyfile同樣支持json,兩種形式可以用caddy相互轉換
│ ├── lexer.go // 詞法分析器
│ ├── parse.go // 讀入配置文件並使用lexer進行解析
├── caddyhttp // caddy的http服務器
│ ├── caddyhttp.go // 用來加載插件,import了所有caddy http服務器相關的指令(中間件)
│ ├── httpserver
│ │ ├── error.go // 定義了常見的錯誤,都實現了error接口
│ │ ├── https.go // 處理https相關邏輯
│ │ ├── logger.go // 日誌相關邏輯
│ │ ├── plugin.go // httpserver插件邏輯,定義了一個directives的字符串slice,自定義插件時,這裏要改!!
│ │ ├── server.go // HTTP server的實現,包裹了一層標準庫的http.Server
│ │ ├── ...
│ ├── bind
│ ├── browse
│ ├── errors
│ ├── basicauth
│ ├── ...
├── caddytls // caddy tls 相關邏輯,不影響主要流程先不看。
├── commands.go // 命令行終端命令的處理邏輯,處理終端執行的時候加入的參數。
├── controller.go // 用於從配置caddyfile中的配置來設置directive
├── onevent // 插件,on在觸發指定事件時執行命令。舉個栗子:在服務器啓動時,啓動php-fpm。
├── plugins.go // 維護了caddy的所有插件,event hook等
├── rlimit_nonposix.go
├── rlimit_posix.go // 啓動服務器的時候,如果文件句柄數限制過低就提醒你設置ulimits
├── sigtrap.go // 信號機關,用來處理信號,如中斷、掛起等。
├── sigtrap_nonposix.go
├── sigtrap_posix.go
├── telemetry // 遙測,就是監控。個人覺得使用prometheus的exporter做更好。
└── upgrade.go // 熱更新
Caddy 啓動過程
main 入口
caddy/main.go
package main
import "github.com/caddyserver/caddy/caddy/caddymain"
var run = caddymain.Run // replaced for tests
func main() {
run()
}
main函數很簡單,引用了caddmain包,調用了caddy/caddymain/run.go的run函數。下面主要看run函數怎麼去啓動服務器的:
run函數
run函數代碼比較長,首先大致看下run.go文件裏有哪些東西。包內函數(首字母小寫的)先忽略,因爲最能提現一個包的主要職責的是它的import、包內變量、init函數、導出函數(首字母大寫的函數)、導出結構體(首字母大寫的結構體)。
package caddymain
import (
...
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
// This is where other plugins get plugged in (imported)
)
const appName = "Caddy" // 這裏定義了app的名字。
// Flags that control program flow or startup
// 定義了程序啓動的一些參數,這些參數從運行時指定(從os.Args中解析)。
// 這裏定義的是程序啓動後,就不能更改的。 配置文件中的參數是程序啓動後,還可以熱更新的。
var (
serverType string
conf string
cpu string
envFile string
...
)
// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true // 遙測啓動開關,不是重點,忽略。
func init() {...}
// Run is Caddy's main() function.
func Run() {...}
根據執行順序來看,main包import了caddymain這個包,且調用了caddymain.Run。那麼執行步驟如下:
- import caddymain包的相關依賴,這裏需要看看import卻沒有使用的caddyhttp包做了什麼操作。
可以看到caddyhttp包都是在import其他的包, 這些被caddyhttp import的包,就是caddy官方自帶的相關插件,和httpserver這個server plugin。
稍後再看plugin的具體實現。 - 初始化caddymain包中的包內變量。
- 執行caddymain包的init函數。
不管什麼包,init函數的作用都很明確:初始化相關包內變量,讀取相關配置、參數等等。caddymain的init函數也不例外。func init() { caddy.TrapSignals() // 捕捉信號量,處理 // 解析啓動參數,flag包是從os.Args中解析的。 flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory") flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", sable") ... // 註冊加載caddyfile的loader. caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader)) caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader)) }
- 調用Run函數。
在caddy這個項目中,caddymain.Run纔是程序的"main"函數。
這裏忽略了一些無關緊要的邏輯和某些執行一次就退出的命令,如caddy -plugins 、 caddy -validate
只關注caddy服務器啓動過程相關的步驟。
func Run() {
// 1. 解析命令行參數,不調用這個的話,init裏面綁定的變量就都是默認值了。
flag.Parse()
// 2. log怎麼輸出,輸出到哪裏,日誌文件怎麼平滑滾動。
// Set up process log before anything bad happens
switch logfile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
...
}
// 3. 加載環境變量並設置。
// load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err)
}
// 4. 初始化遙測相關邏輯
// initialize telemetry client
if EnableTelemetry {
err := initTelemetry()
...
}
...
// 5. 可以把caddyfile從json和普通模式互相轉換
// Check if we just need to do a Caddyfile Convert and exit
checkJSONCaddyfile()
// 6. 設置cpu使用,最小不能小於1
// Set CPU cap
err := setCPU(cpu)
if err != nil {
mustLogFatalf("%v", err)
}
// 7. 發送Startup事件,然後調用EventHook去處理這個事件。
// EventHook處理過程要用goroutine去處理,防止阻塞。
// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)
// 8. 去加載caddyfile文件,根據插件定義的loader去加載。
// 詳細內容可以看LoadCaddyfile的函數備註
// Get Caddyfile input
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
if err != nil {
mustLogFatalf("%v", err)
}
// 9. 啓動服務器!!!
// Start your engines
instance, err := caddy.Start(caddyfileinput)
if err != nil {
mustLogFatalf("%v", err)
}
// 10. 阻塞主進程,防止main goroutine退出
// 內部就是調用了sync.WaitGroup的Wait方法。
// Twiddle your thumbs
instance.Wait()
}
Run函數的邏輯也很清楚: 1.處理參數,讀取配置 2. 調用Start方法 3. Wait阻塞main goroutine.
Start 函數
Start函數位於 項目根目錄下的 caddy.go文件中。
爲什麼要這樣組織文件結構呢? 爲什麼不把start函數也放在caddymain包裏面呢?
這是爲了方便別的項目來引用caddy。如果放在caddymain,別的項目引入的時候就只需要import "github.com/caddyserver/caddy"
從這裏可以看出caddy這個項目的定位,不僅僅是一個web server, 還可以是一個lib庫。
// Start starts Caddy with the given Caddyfile.
//
// This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) {
// 1. 初始化一個instance, Run函數末尾調用的instance.Wait()就是調用的這個instance裏面的wg。
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
// 2. 根據這個instance和caddyfile的配置啓動服務器
err := startWithListenerFds(cdyfile, inst, nil)
if err != nil {
return inst, err
}
// 3. 給父進程發送成功啓動信號
// 這裏只要在upgrade的時候纔有用,upgrade的時候父進程fork子進程,子進程成功執行完startWithListenerFds後,通過管道發送success給父進程,父進程再kill self。
signalSuccessToParent()
if pidErr := writePidFile(); pidErr != nil {
log.Printf("[ERROR] Could not write pidfile: %v", pidErr)
}
// 4. 發送instance start up 事件,調用對此事件感興趣的hook函數。
// Execute instantiation events
EmitEvent(InstanceStartupEvent, inst)
return inst, nil
}
到這一步整個服務器還沒有運作起來,還無法監聽端口,處理請求。startWithListenerFds函數裏面開始用Caddyfile文件定義的配置啓動相關的Services(注意是複數,有多少個Server取決於Caddyfile裏面的定義)。
startWithListenerFds 函數
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
// 1. 這裏把instance保存到了一個包內變量instances的slice裏面,
// 一個instance代表服務的實例,如http服務器 dns服務器等。
// 對instances的操作都加上鎖了,防止併發問題。
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
var err error
defer func() {
// 當instance處理失敗了,需要從instances 這個slice中移除。
if err != nil {
instancesMu.Lock()
for i, otherInst := range instances {
if otherInst == inst {
instances = append(instances[:i], instances[i+1:]...)
break
}
}
instancesMu.Unlock()
}
}()
// 2. 這裏處理Caddyfile, 驗證Caddyfile裏面的directives(指令)是否有效可用。
if cdyfile == nil {
cdyfile = CaddyfileInput{}
}
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
if err != nil {
return err
}
// 3. 這裏是Make Servers,就是產生instance裏面所有的server
// 比如instance代表了一個http服務器, serverA就是其中監聽在8080端口的一個http服務,
// serverB是另一個監聽在8020的http服務。 這裏同時被make出來。
slist, err := inst.context.MakeServers()
if err != nil {
return err
}
// 4. 處理start up的相關callback, 先不關注其中的細節。
...
// 5. 上面創建了servers 這裏統一啓動起來
err = startServers(slist, inst, restartFds)
if err != nil {
return err
}
// 6. 處理after start up的相關callback, 先不關注其中的細節。
...
mu.Lock()
started = true
mu.Unlock()
return nil
}
startWithListenerFds調用了MakeServers() 產生若干個(具體有多少個,看Caddyfile怎麼定義)服務的實例,startServers又把這些服務實例啓動起來。 而且在啓動服務前後,還會執行一些callback函數。
執行邏輯順序如下:
- MakeServers()
MakeServers是plugins.go文件內Context接口的一個方法,放在接口裏的作用,自然是方便擴展。plugins.go被放在根目錄下,說明caddy可以很支持外部自定義其他ServerType的Instance。
因爲golang的接口和實現是松耦合的,很難從接口定義去找到實現它的實例,反過來也是。這裏想找到MakeServers的實現,一可以通過邏輯判斷,二可以通過IDE的全局搜索功能。
Caddy項目自帶了http這種ServerType的實現,於是去caddyhttp/httpserver裏面找,果不其然在caddyhttp/httpserver/plugin.go裏面找到了。
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
// 這裏用到"github.com/mholt/certmagic"這個包來做tls和https相關的事情,
// 因爲沒有實際用過certmagic, 這裏的代碼也跳過。
// CertMagic - 利用Go程序管理TLS證書的頒發和續訂,自動化添加HTTPS
...
// 前面講過每個server實例綁定了一個端口,這裏是把配置分組,按照端口不同來分組
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
if err != nil {
return nil, err
}
// 根據每個端口,和定義在這個端口上的相關配置來生成一個Server實例
var servers []caddy.Server
for addr, group := range groups {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
// 判斷是dev還是prod環境
deploymentGuess := "dev"
if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction {
deploymentGuess = "prod"
}
telemetry.Set("http_deployment_guess", deploymentGuess)
telemetry.Set("http_num_sites", len(h.siteConfigs))
return servers, nil
}
- run startup callbacks
(和hook、callback相關的先不分析。) - startServers()
MakeServer函數被抽象成接口了,爲什麼startServer沒有呢? 這是因爲Server本身就是一個接口, startServers的主要邏輯其實就是調用Listen 和 Serve。
type Server interface {
TCPServer
UDPServer
}
type TCPServer interface {
Listen() (net.Listener, error)
Serve(net.Listener) error
}
type UDPServer interface {
ListenPacket() (net.PacketConn, error)
ServePacket(net.PacketConn) error
}
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
// 服務啓動或處理過程產生的錯誤就往這個channel中塞
errChan := make(chan error, len(serverList))
// 用來控制記錄錯誤日誌的goroutine在記錄完日誌後能退出
stopChan := make(chan struct{})
// 保證控制server異常退出後,所有錯誤日誌能被記錄到
stopWg := &sync.WaitGroup{}
// 根據傳入的server list 遍歷處理
// 每個server綁定相應的tcp listener和udp listener。並將server添加到instance裏面
// TODO: upgrade和reload過程中文件描述符的操作沒有看懂, 先省略,後續文章補上
for _, s := range serverList {
var (
ln net.Listener
pc net.PacketConn
err error
)
...
if ln == nil {
ln, err = s.Listen()
if err != nil {
return fmt.Errorf("Listen: %v", err)
}
}
if pc == nil {
pc, err = s.ListenPacket()
if err != nil {
return fmt.Errorf("ListenPacket: %v", err)
}
}
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
}
// 遍歷instance的server, 調用server的Serve方法監聽。出錯的話就把錯誤塞入errChan這個管道里面
// 每個server都起了兩個goroutine,分別監聽tcp和udp.
// 這裏使用WaitGroup來同步goroutine,
// instance的wg用來防止main goroutine退出。
// stopWg用來掛起最下面那個goroutine。
// 這樣能保證,只要有一個server還在監聽着,就不會導致main goroutine退出,也不會導致記錄錯誤日誌的goroutine退出。
for _, s := range inst.servers {
inst.wg.Add(2)
stopWg.Add(2)
func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
go func() {
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.Serve(ln)
}()
go func() {
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.ServePacket(pc)
}()
}(s.server, s.listener, s.packet, inst)
}
// 這個goroutine用來記錄從errChan來的錯誤
go func() {
for {
select {
case err := <-errChan:
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") {
// this error is normal when closing the listener; see https://github.com/golang/go/issues/4373
log.Println(err)
}
}
case <-stopChan:
return
}
}
}()
// 這個goroutine用來控制,當所有server都退出後,停止上面那個記錄錯誤日誌的goroutine.
go func() {
stopWg.Wait()
stopChan <- struct{}{}
}()
return nil
}
- run any AfterStartup callbacks
(和hook、callback相關的先不分析。)
以上就是Caddy服務啓動的過程。
TODO: 有些詳細的實現還沒有具體看完,之後再其他文章裏面詳細講解。