豌豆夹Redis解决方案Codis源码剖析:Dashboard

豌豆夹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("/hello", (req, res) -> "Hello World");
    }
}

2.3 Slot的角色

在开始剖析数据分布和迁移的源码之前,先说一下Codis中Slot的概念。Slot是Codis的数据分配、迁移等操作的基本单位,可以把Slot看成一致性哈希中的Bucket、VNode等概念。客户端传来的Key经过某种hash函数(Codis用的是Crc32)对应到某个Slot中,因为哈希函数是一定的所以这个对应关系是无法改变的。但我们可以改变的是Slot与Group的对应关系,这样当新增Group时就可以通过调整Slot的分布达到负载均衡的效果。

不幸的是,Redis中并没有Slot这个概念,也就是说:虽然我们给Key分配好了Slot,但是一旦存入Redis后Key属于哪个Slot这个信息就丢失了。解决的方法有很多种,比如:

  • 1)在ZooKeeper中保存Key与Slot的对应关系,需要时就查询一下。这种方式类似HDFS中的NameNode,缺点是Key很多时会占用很多空间。
  • 2)数据迁移时遍历所有Key,用哈希函数现去算一下Key所属的Slot是否要迁移,缺点是迁移时计算量比较大,而且每个Slot迁移时都要去算可能有很多重复的计算量。
  • 3)保存到Redis之前在Key或Value中加入一些隐含信息,缺点是会改变业务的数据。
  • 4)修改Redis源码加入Slot的概念,在Redis中保存Key属于的Slot,并提供基于Slot的Migrate原子操作。Codis采取的是这种做法,缺点就是要修改Redis源码,以后升级Redis比较麻烦,尤其像Codis没有将改动封装到一个动态链接库则可能更为麻烦。
  • 5)利用Redis中database的概念替代Slot,GitHub上的xcodis采取的就是这种思想。缺点是每个Slot对应的Redi
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值