豆瓣Redis解决方案Codis源码剖析:Dashboard,rediscodis
豆瓣Redis解决方案Codis源码剖析:Dashboard
1.不只是Dashboard
虽然名字叫Dashboard,但它在Codis中的作用却不可小觑。它不仅仅是Dashboard管理页面,更重要的是,它负责监控和指挥各个Proxy的负载均衡(数据分布和迁移)。并且,所有API都以RESTFul接口的形式对外提供,供Proxy和codis-config(Codis的命令行工具)调用。下面就来看一下数据分布和迁移的代码执行流程。
Dashboard涉及到的知识点比较多,包括Martini框架、Model模型层、数据的负载均衡分配、Redis中Slot的实现、ZooKeeper的Sequential结点实现消息队列、迁移过程中的数据访问等等。与此同时,本篇还记录了在研究过程中的发散思考,例如Java中的AR模型层。虽然内容稍有些庞杂,但都是真实的记录,相信于人于己都会有很大帮助。
2.Dashboard
2.1 codis-config命令行
codis-config是cmd/cconfig下编译出的命令行工具,它以命令行的形式提供对Codis常用命令的支持,例如启动Dashboard、初始化slot以及migrate,后两者会被封装成REST请求发送到Dashboard执行。下面简单看一下main.go的源码:
func main() {
...
args, err := docopt.Parse(usage, nil, true, "codis config v0.1", true)
if err != nil {
log.Error(err)
}
// set config file
var configFile string
var config *cfg.Cfg
if args["-c"] != nil {
configFile = args["-c"].(string)
config, err = utils.InitConfigFromFile(configFile)
if err != nil {
Fatal(err)
}
} else {
config, err = utils.InitConfig()
if err != nil {
Fatal(err)
}
}
// load global vars
globalEnv = env.LoadCodisEnv(config)
cmd := args["<command>"].(string)
cmdArgs := args["<args>"].([]string)
go http.ListenAndServe(":10086", nil)
err = runCommand(cmd, cmdArgs)
}
func runCommand(cmd string, args []string) (err error) {
argv := make([]string, 1)
argv[0] = cmd
argv = append(argv, args...)
switch cmd {
case "action":
return errors.Trace(cmdAction(argv))
case "dashboard":
return errors.Trace(cmdDashboard(argv))
case "server":
return errors.Trace(cmdServer(argv))
case "proxy":
return errors.Trace(cmdProxy(argv))
case "slot":
return errors.Trace(cmdSlot(argv))
}
return errors.Errorf("%s is not a valid command. See 'codis-config -h'", cmd)
}
2.2 Dashboard启动
大家是否还记得,在安装Codis完之后,我们执行了smaple目录下的一系列脚本初始化Codis并启动服务,其中有一项./start_dashboard.sh就是启动Dashboard服务。它是调用的codis-config,传入的参数”dashboard”正好对应上面的main.go中的cmdDashboard()。
#!/bin/sh
nohup ../bin/codis-config -c config.ini -L ./log/dashboard.log dashboard --addr=:18087 --http-log=./log/requests.log &>/dev/null &
打开dashboard.go就能看到cmdDashboard()的实现,它调用的是下面的runDashboard()函数:
func runDashboard(addr string, httpLogFile string) {
log.Info("dashboard listening on addr: ", addr)
m := martini.Classic()
...
m.Use(martini.Static(filepath.Join(binRoot, "assets/statics")))
m.Use(render.Renderer(render.Options{
Directory: filepath.Join(binRoot, "assets/template"),
Extensions: []string{".tmpl", ".html"},
Charset: "UTF-8",
IndentJSON: true,
}))
m.Get("/api/server_groups", apiGetServerGroupList)
m.Get("/api/overview", apiOverview)
m.Get("/api/redis/:addr/stat", apiRedisStat)
m.Get("/api/redis/:addr/:id/slotinfo", apiGetRedisSlotInfo)
m.Get("/api/redis/group/:group_id/:slot_id/slotinfo", apiGetRedisSlotInfoFromGroupId)
...
// create temp node in ZK
if err := createDashboardNode(); err != nil {
Fatal(err)
}
defer releaseDashboardNode()
// create long live migrate manager
conn := CreateZkConn()
defer conn.Close()
globalMigrateManager = NewMigrateManager(conn, globalEnv.ProductName(), preMigrateCheck)
defer globalMigrateManager.removeNode()
m.RunOnAddr(addr)
}
值得注意的是,Dashboard使用的是Martini框架,非常容易就能暴露各种RESTFul接口。这里不深入研究Martini框架的用法了,但提一个Java中的“山寨Martini”框架-SparkJava。Spark这个名字实在太火了,这个框架跟分布式内存计算的那个Spark框架可没有一点关系。稍有些遗憾的是,使用SparkJava的前提是必须安装JDK 8,因为SparkJava大量使用了JDK 8中的特性:
import static spark.Spark.*;
// Visit http://localhost:4567/hello
public class HelloWorld {
public static void main(String[] args) {
get(

