学习RadonDB源码(一)
1. 可能是开始也可能是结束
radondb是国内知名云服务提供商青云开源的一款产品,下面是一段来自官方的介绍:
qingcloud radondb 是基于 mysql 研发的新一代分布式关系型数据库,可无限水平扩展,支持分布式事务,具备金融级数据强一致性,满足企业级核心数据库对大容量、高并发、高可靠及高可用的极致要求。
做dba的都知道关系型数据库在分布式数据库方面堪称举步维艰,虽然很多高手或者公司都开源了自己的中间件,但是很少有公司像青云这样将自己商用的成套解决方案直接开源的。可能开源版本和商用版本之间有很多功能差异,不过从解决方案的完整性角度来看,radondb堪称是良心产品了。
而且radondb的还有一个明显的好处是用go编写的,而且现在的代码量也不算大,对于一个学习go语言的人来说这是一个极好的项目。另外还有一点,radondb模拟了完整的mysql server端,里面有一项核心的东西叫做sql解析器和优化器的,刚好可以借此机会从源码角度学习一下其思想。要知道mysql虽然开源,但是整个项目都是用c编写的,很难看懂。
我打算用闲暇时间好好学习一下radondb源码,当然我可能半途而废,所以,这一篇可能是开始也可能是结束。
2. 入口的radon.go文件
这个文件在“radon/src/radon”目录下,代码只有区区82行,不过这是整个radondb的入口。
这段代码中利用了不少flag包用于接收参数,首先映入眼帘的是一堆import,此处就不加赘述了,因为毕竟只是引入了包,至于做什么的,代码写了就能知道。
接下来是包的初始化:
var ( flagconf string ) func init() { flag.stringvar(&flagconf, "c", "", "radon config file") flag.stringvar(&flagconf, "config", "", "radon config file") }
flag是一个很好用的包,用于接收命令行参数,至于怎么用可以参考网上的资料。这个init()函数很有意思,这个函数会在很多书的“包初始化”一节来讲述,其实记住几个顺序就可以:
- 初始化导入的包;
- 在包级别为声明的变量计算并分配初始值;
- 执行包内的init函数。
这是包的初始化顺序,那么回到radon.go,初始化顺序也是一目了然的。
init函数不能被引用
接下来是一个简单的usage函数:
func usage() { fmt.println("usage: " + os.args[0] + " [-c|--config] <radon-config-file>") }
仅仅是为了打印命令行的帮助,在引用的时候才有效,现在只是声明。
而后就是程序的主入口main函数了,这段函数的最开始就执行了这样一句:
runtime.gomaxprocs(runtime.numcpu())
声明了逻辑处理单元,数量和cpu核数相当,这一点在之前讲goroutine的笔记中讲述过。
紧接着,程序将获得一些关键的环境信息:
build := build.getinfo()
虽然只有一句,但是背后的东西还是很丰富的:
func getinfo() info { return info{ goversion: runtime.version(), tag: "8.0.0-" + tag, time: time, git: git, platform: platform, } }
这是一种典型的结构体的初始化方式,如果对结构体不熟悉,建议也是百度一下相关资料。
这些打印出信息的东西无非就是一些显示输出,跟我们平时启动spring的时候打印那个炫酷的spring banner没什么区别,接来下才是处理一些要紧的东西,比如处理配置:
// config flag.usage = func() { usage() } flag.parse() if flagconf == "" { usage() os.exit(0) } conf, err := config.loadconfig(flagconf) if err != nil { log.panic("radon.load.config.error[%v]", err) } log.setlevel(conf.log.level)
其中的flag.usage
是函数变量,函数变量是一个新颖的概念,举一个例子说明:
func square(n int) int { return n*n } f := square //打印9 fmt.println(f(3))
flag包中的usage本身就是个函数变量。
上面这段业务代码主要做了这么几件事情:
- 解析flag,得到命令行参数;
- 判断参数是否为空,为空则打印使用说明并退出;
- 加载配置项,并做异常处理;
- 设置日志级别。
我们先不说紧接着要启动的monitor了,这是一个性能指标监控,并不在我的学习范围内。
// proxy. proxy := proxy.newproxy(log, flagconf, build.tag, conf) proxy.start()
代理是每个人写程序都挺喜欢写的名字。proxy是一个自行编写的包,我们来看看newproxy的时候做了什么:
func newproxy(log *xlog.log, path string, serverversion string, conf *config.config) *proxy { audit := audit.newaudit(log, conf.audit) router := router.newrouter(log, conf.proxy.metadir, conf.router) scatter := backend.newscatter(log, conf.proxy.metadir) syncer := syncer.newsyncer(log, conf.proxy.metadir, conf.proxy.peeraddress, router, scatter) plugins := plugins.newplugin(log, conf, router, scatter) return &proxy{ log: log, conf: conf, confpath: path, audit: audit, router: router, scatter: scatter, syncer: syncer, plugins: plugins, sessions: newsessions(log), iptable: newiptable(log, conf.proxy), throttle: xbase.newthrottle(0), serverversion: serverversion, } }
这段代码倒是很简单,就是利用入参中的配置项,声明了一系列的变量,并将这些变量封装在一个结构体内,然后返回。至于这些变量都是干什么的,我下次再说,这次只跟踪主流程。
紧接着看看启动都做了什么:
// start used to start the proxy. func (p *proxy) start() { log := p.log conf := p.conf audit := p.audit iptable := p.iptable syncer := p.syncer router := p.router scatter := p.scatter plugins := p.plugins sessions := p.sessions endpoint := conf.proxy.endpoint throttle := p.throttle serverversion := p.serverversion log.info("proxy.config[%+v]...", conf.proxy) log.info("log.config[%+v]...", conf.log) if err := audit.init(); err != nil { log.panic("proxy.audit.init.panic:%+v", err) } // 省略了一大堆,为了节省篇幅 spanner := newspanner(log, conf, iptable, router, scatter, sessions, audit, throttle, plugins, serverversion) if err := spanner.init(); err != nil { log.panic("proxy.spanner.init.panic:%+v", err) } svr, err := driver.newlistener(log, endpoint, spanner) if err != nil { log.panic("proxy.start.error[%+v]", err) } p.spanner = spanner p.listener = svr log.info("proxy.start[%v]...", endpoint) go svr.accept() }
这个start函数看起来好像java中的构造器,做的事情也和构造器有点相似,就是赋值,不过它还能做多的事情,比如说启动了一个监听:
svr, err := driver.newlistener(log, endpoint, spanner)
有了监听之后,就可以启动一个goroutine了,而且是有条件的存活的:
go svr.accept()
这里的条件就是accept要做什么:
accept runs an accept loop until the listener is closed.
在listener关闭之前,accept将始终运行一个循环,也就是说这个goroutine会一直生存下去。
到这一步proxy就算启动起来了,然后就会去启动admin了:
// admin portal. admin := ctl.newadmin(log, proxy) admin.start()
按照惯例看看newadmin在干什么:
// newadmin creates the new admin. func newadmin(log *xlog.log, proxy *proxy.proxy) *admin { return &admin{ log: log, proxy: proxy, } }
代码逻辑很简单,就是返回一个admin结构体的指针。而admin结构体是这样的:
// admin tuple. type admin struct { log *xlog.log proxy *proxy.proxy server *http.server }
看,之前的代码里没有对server进行赋值,这是为什么?答案在start函数里:
// start starts http server. func (admin *admin) start() { api := rest.newapi() router, err := admin.newrouter() if err != nil { panic(err) } api.setapp(router) handlers := api.makehandler() admin.server = &http.server{addr: admin.proxy.peeraddress(), handler: handlers} go func() { log := admin.log log.info("http.server.start[%v]...", admin.proxy.peeraddress()) if err := admin.server.listenandserve(); err != http.errserverclosed { log.panic("%v", err) } }() }
这里是一系列的http操作,对server的赋值就在其中,此时会把默认ip,端口等等信息都写入到server中:
一看代码我就知道radondb要用3308端口进行连接,而起管理端口就注册在8080。
好了,这些都很容易明白,此时start函数只需要启动一个goroutine就可以了。关键在这里:
看名字就知道这是干什么的,监听并维护一个服务,看看其注释:
那么这样一来,服务就启动起来了,当然后面还会有stop函数,就不再详解了。有意思的是,可以注意这几句:
// handle sigint and sigterm. ch := make(chan os.signal) signal.notify(ch, syscall.sigint, syscall.sigterm) log.info("radon.signal:%+v", <-ch)
这几句声明了一个通道,一个signal类型的通道,可以用于接收系统调用,sigint一般是ctrl-c,sigterm一般是kill。在发生这两个系统调用后,系统开始关闭。
3. 小结
go语言还是简单的,至少现在看来,这些代码我是都能看懂的,而我学习go语言的时间也不过两周。
我希望能借着radondb的开源,学会关键的优化器和sql解析器的思想。