Caddy 源碼閱讀

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。那麼執行步驟如下:

  1. import caddymain包的相關依賴,這裏需要看看import卻沒有使用的caddyhttp包做了什麼操作。
    在這裏插入圖片描述
    可以看到caddyhttp包都是在import其他的包, 這些被caddyhttp import的包,就是caddy官方自帶的相關插件,和httpserver這個server plugin。
    稍後再看plugin的具體實現。
  2. 初始化caddymain包中的包內變量。
  3. 執行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))
    	}
    
  4. 調用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函數。
執行邏輯順序如下:

  1. 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
}
  1. run startup callbacks
    (和hook、callback相關的先不分析。)
  2. 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
}
  1. run any AfterStartup callbacks
    (和hook、callback相關的先不分析。)

以上就是Caddy服務啓動的過程。

TODO: 有些詳細的實現還沒有具體看完,之後再其他文章裏面詳細講解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章