设为首页 加入收藏

TOP

用Go造轮子-管理集群中的配置文件(一)
2017-09-30 13:24:08 】 浏览:3962
Tags:轮子 管理 集群 配置 文件

写在前面

最近一年来,我都在做公司的RTB广告系统,包括SSP曝光服务,ADX服务和DSP系统。因为是第一次在公司用Go语言实现这么一个大的系统,中间因为各种原因造了很多轮子。现在稍微有点时间,觉着有必要总结这一年来用Go造轮子的经验和不足。

集群中遇到的配置文件管理问题

RTB广告系统中涉及到的服务程序并不算很多,但是因为RTB系统会面临很多的流量,而且为了确保可用性,最基本的就是多实例组成集群,同时考虑到后续业务增长,集群的扩缩容也是要做的。我们在设计的时候,基于ZoooKeeper做了服务发现,而我们的服务接入依靠Nginx集群,然后通过反向代理把请求负载均衡到不同的服务实例中。这里就存在以下问题:

  • 当我们升级某个服务时,如何通知Nginx集群自动的摘除或者添加该服务实例,保证我们的升级不会影响到业务和用户体验
  • 进一步,任意一个服务集群内的配置数据该如何自动更新和应用?

业界方案

业界其实有很多成熟的方案解决这类问题:

  • 比如开源项目consul-template,但是这个工具只支持后端consul,而我们用的是ZooKeeper
  • 再比如confd,可以支持多种后端,比如etcd或者zookeeper,但是它用的ZooKeeper客户端不支持在故障时对业务请求进行重试,比如发起了一个GetW请求,而Session变成超时状态,这个时候GetW返回的Channel就不可用了,只能重新发起请求,但是重试多少次请求其实是不知道的,针对这个情况,我还在项目一开始的时候实现了新的包,加入了对业务层透明的重试机制。

整体工作流程

  1. 解析模版,获取要动态查询的节点
  2. 向指定的服务器,比如ZooKeeper发起查询请求,并观察指定节点的变化
  3. 当第一次或者节点发生变更后,查询最新数据
  4. 把最新数据应用到模版中生成新的配置文件数据
  5. 保存最新的配置文件数据到目标路径,并调用指定的命令应用最新的配置文件

实现

模版机制

Go官方标准库提供了Template包,支持if, range等控制语句,也支持用户自定义方法。模版机制的方便之处在于,它本身是一种DSL,也算是一种支持计算的超级printf。举例如下:

package main

import (
    "os"
    "text/template"
)

var tplContent = `
 {{range service "apiGateWay" }}
 server {{.Name}} {{.ID}} {{.Address}}
 {{end}}`

type ApiGateWayService struct {
    Name    string
    ID      string
    Address string
}

func main() {
    tmpl, err := template.New("test.template").Funcs(template.FuncMap{
        "service": func(serviceName string) []ApiGateWayService {
            return []ApiGateWayService{
                {
                    Name:    "test",
                    ID:      "1",
                    Address: "192.168.1.100:9200",
                },
            }
        },
    }).Parse(tplContent)
    if err != nil {
        panic(err)
    }

    tmpl.Execute(os.Stdout, nil)、
}

上面的代码实际上是做了类似这样的过程,为了简单描述,我还是直接写一段GO代码

package main

import (
    "fmt"
    "os"
)

type ApiGateWayService struct {
    Name    string
    ID      string
    Address string
}

func service(serviceName string) []ApiGateWayService {
    return []ApiGateWayService{
        {
            Name:    "test",
            ID:      "1",
            Address: "192.168.1.100:9200",
        },
    }
}

func main() {
    for _, v := range service("apiGateWay") {
        fmt.Fprintf(os.Stdout, "server %s %s %s", v.Name, v.ID, v.Address)
    }
}

解析和渲染模版

上面的代码中,模版内容如下:

var tplContent = `
 {{range service "apiGateWay" }}
 server {{.Name}} {{.ID}} {{.Address}}
 {{end}}`
  • 当我们拿到这么一个模板时,我们是不知道它是不是合法的,也许有语法错误,所以得先需要校验。这个我们可以调用template.Parse方法进行解析、校验语法。
  • 当语法没有问题时,我们就开始进行渲染。在我们这个示例中,模版引擎会在渲染时调用service方法并对其返回结果进行循环处理,然后输出相应的数据到一个io.Writer中。service方法主要功能是根据传入的服务名,去ZK中查询相应的节点的所有子节点的数据,然后返回相应的数据。
    1. 当我们程序第一次运行时,实际上我们还没准备好指定服务的数据,但是我们至少在这一次知道了它要查询哪个服务的数据,并且我们这时可以启动goroutine去后台轮询查询数据,并把这些数据放入到缓存中
    2. 那么当我们之后再渲染时,就可以直接缓存中查询指定服务的所有实例(也就是子节点)的数据,然后就可以渲染出最终想要的配置文件数据。之后就可以保存了。

整个代码实现中略微复杂,其中的核心既不是如何缓存,也不是后台如何查询,而是要记录下未知的服务名,以及假如已经知道了服务名并且缓存了数据,如何从缓存中查询数据,这个过程还是拿service方法举例,代码如下:

func serviceFunc(tracker *DataTracker, used, missing map[string]dependency.Dependency) func(...string) ([]dependency.Service, error) {
    return func(s ...string) ([]dependency.Service, error) {
        var r []dependency.Service

        if len(s) == 0 || s[0] == "" {
            return r, nil
        }

        d, err := dependency.ParseService(s...)
        if err != nil {
            return nil, err
        }

        addDependency(used, d)

        data, ok := tracker.Get(d)
        if ok {
            return data.([]dependency.Service), nil
        }

        addDependenc
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Golang 在 Mac、Linux、Windows .. 下一篇golang 栈操作

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目