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的功能

Caddy 源码阅读_第1张图片
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: 有些详细的实现还没有具体看完,之后再其他文章里面详细讲解。

你可能感兴趣的:(golang)