用Go造轮子-管理集群中的配置文件
写在前面
最近一年来,我都在做公司的RTB广告系统,包括SSP曝光服务,ADX服务和DSP系统。因为是第一次在公司用Go语言实现这么一个大的系统,中间因为各种原因造了很多轮子。现在稍微有点时间,觉着有必要总结这一年来用Go造轮子的经验和不足。
集群中遇到的配置文件管理问题
RTB广告系统中涉及到的服务程序并不算很多,但是因为RTB系统会面临很多的流量,而且为了确保可用性,最基本的就是多实例组成集群,同时考虑到后续业务增长,集群的扩缩容也是要做的。我们在设计的时候,基于ZoooKeeper做了服务发现,而我们的服务接入依靠Nginx集群,然后通过反向代理把请求负载均衡到不同的服务实例中。这里就存在以下问题:
- 当我们升级某个服务时,如何通知Nginx集群自动的摘除或者添加该服务实例,保证我们的升级不会影响到业务和用户体验
 - 进一步,任意一个服务集群内的配置数据该如何自动更新和应用?
 
业界方案
业界其实有很多成熟的方案解决这类问题:
- 比如开源项目consul-template,但是这个工具只支持后端consul,而我们用的是ZooKeeper
 - 再比如confd,可以支持多种后端,比如etcd或者zookeeper,但是它用的ZooKeeper客户端不支持在故障时对业务请求进行重试,比如发起了一个GetW请求,而Session变成超时状态,这个时候GetW返回的Channel就不可用了,只能重新发起请求,但是重试多少次请求其实是不知道的,针对这个情况,我还在项目一开始的时候实现了新的包,加入了对业务层透明的重试机制。
 
整体工作流程
- 解析模版,获取要动态查询的节点
 - 向指定的服务器,比如ZooKeeper发起查询请求,并观察指定节点的变化
 - 当第一次或者节点发生变更后,查询最新数据
 - 把最新数据应用到模版中生成新的配置文件数据
 - 保存最新的配置文件数据到目标路径,并调用指定的命令应用最新的配置文件
 
实现
模版机制
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中查询相应的节点的所有子节点的数据,然后返回相应的数据。
- 当我们程序第一次运行时,实际上我们还没准备好指定服务的数据,但是我们至少在这一次知道了它要查询哪个服务的数据,并且我们这时可以启动goroutine去后台轮询查询数据,并把这些数据放入到缓存中
 - 那么当我们之后再渲染时,就可以直接缓存中查询指定服务的所有实例(也就是子节点)的数据,然后就可以渲染出最终想要的配置文件数据。之后就可以保存了。
 
 
整个代码实现中略微复杂,其中的核心既不是如何缓存,也不是后台如何查询,而是要记录下未知的服务名,以及假如已经知道了服务名并且缓存了数据,如何从缓存中查询数据,这个过程还是拿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
		}
		addDependency(missing, d)
		return r, nil
	}
}
其中tracker表示的是缓存对象,used和missing是一个map,其中value类型是dependency.Dependency, key为一个Dependency的HashCode,也就是一个唯一身份标识。
Dependency是一个接口,主要用于查询数据,是一个阻塞过程。serviceFunc返回一个lambda,内部主要主要的事情是记录哪个Dependency对象用到了,并且尝试从tracker中查询缓存数据,如果有就返回,没有更新missing,记录Dependency对象没有相应的缓存数据,然后返回空数据。
数据查询
当我们完成了解析和渲染过程后,我们要检查渲染过程中记录的missing是否不为空,如果不空,就需要发起后台查询进行处理,大体过程如下:
if len(missing) > 0 {
    for _, v := range missing {
        if !r.watcher.Watching(v) {
            r.watcher.Add(v)
            log.Debug("try to watch dependency", v.HashCode())
        }
    }
    continue
}
其中每个Dependency对象都有一个Fetch方法,当通过Watcher.Add方法时,就会启动一个独立的goroutine负责调用Dependency.Fetch方法,然后通过channel把Dependency.Fetch的结果转给DataTracker进行缓存。
渲染结果保存
- 当渲染模板成功后,就得到了一个新的配置文件数据。首先我们要检查生成的配置文件是否和原有的配置文件有差异,没有差异的就不需要保存了。
 - 其次在保存时,有可能出现任何以外,为了确保出现以外时,能够恢复回来,我们需要对原有的配置文件进行备份。同时,我们为了确保新的配置文件能够落入到磁盘上,每次写入文件后都调用Sync方法强制刷新到磁盘上。主要实现如下:
 
func atomicWrite(path string, contents []byte, perms os.FileMode, backup bool) error {
	parent := filepath.Dir(path)
	if _, err := os.Stat(parent); os.IsNotExist(err) {
		if err := os.MkdirAll(parent, 0755); err != nil {
			return err
		}
	}
	f, err := ioutil.TempFile(parent, "")
	if err != nil {
		return err
	}
	defer os.Remove(f.Name())
	if _, err := f.Write(contents); err != nil {
		return err
	}
	if err := f.Sync(); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	if err := os.Chmod(f.Name(), perms); err != nil {
		return err
	}
	if backup {
		if _, err := os.Stat(path); !os.IsNotExist(err) {
			if err := copyFile(path, path+".bak"); err != nil {
				return err
			}
		}
	}
	if err := os.Rename(f.Name(), path); err != nil {
		return err
	}
	return nil
}
命令执行
当保存好文件后,我们需要调用指定的命令,通知相应的程序加载最新的配置,主要代码如下:
func execute(command string, timeout time.Duration) error {
	var shell, flag string
	if runtime.GOOS == "windows" {
		shell, flag = "cmd", "/C"
	} else {
		shell, flag = "/bin/sh", "-c"
	}
	cmd := exec.Command(shell, flag, command)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Start(); err != nil {
		return err
	}
	done := make(chan error, 1)
	go func() {
		done <- cmd.Wait()
	}()
	select {
	case <-time.After(timeout):
		if cmd.Process != nil {
			if err := cmd.Process.Kill(); err != nil {
				return fmt.Errorf("failed to kill %q in %s: %s", command, timeout, err)
			}
		}
		<-done // Allow the goroutine to finish
		return fmt.Errorf(
			"command %q\n"+
				"did not return for %s - if your command does not return, please\n"+
				"make sure to background it",
			command, timeout)
	case err := <-done:
		return err
	}
}
总结
这个程序目前已经有一年之久,现在回顾头来看,幸好还记得当初的决策原因。通过这个程序,对模版的使用倒是掌握了很多。目前这个轮子功能还较为简单,仅仅实现了一个service方法,但是dependency包是独立抽象的,可以支持任意的存储类型,比如Consul。目前线上运行稳定。
这个工具在Nginx配置管理中,较为方便,当然在一些流量较大的场景中,如果后端服务实例较多,扩缩容时会带来较大的波动,这点可以参考微博团队的Upsync:微博开源基于Nginx容器动态流量管理方案
但是我们没有采用这个方案,考虑到的是基于模版的更新机制更为简单,支持更多的服务,普适性更强,整体也更容易维护。
用Go造轮子-管理集群中的配置文件的更多相关文章
- 管理集群中的 crs 管理员
		
 管理集群中的 crs 管理员 oracle Managing CRS Administrators in the Cluster Use the following commands to ma ...
 - hadoop2集群中关键配置文件的记录
		
配置HDFS 高可用 1.配置HDFS配置文件 $ vi hdfs-site.xml #写入 <configuration> #配置NameService 名字随便起 <prope ...
 - docker swarm英文文档学习-7-在集群中管理节点
		
Manage nodes in a swarm在集群中管理节点 List nodes列举节点 为了查看集群中的节点列表,可以在管理节点中运行docker node ls: $ docker node ...
 - 如何使用Istio 1.6管理多集群中的微服务?
		
假如你正在一家典型的企业里工作,需要与多个团队一起工作,并为客户提供一个独立的软件,组成一个应用程序.你的团队遵循微服务架构,并拥有由多个Kubernetes集群组成的广泛基础设施. 由于微服务分布在 ...
 - MySql集群FAQ----mysql主从配置与集群区别、集群中需要多少台计算机呢?为什么? 等
		
抽取一部分显示在这里,如下, What's the difference in using Clustervs using replication? 在复制系统中,一个MySQL主服务器会更新一个或多 ...
 - 向CDH5集群中添加新的主机节点
		
向CDH5集群中添加新的主机节点 步骤一:首先得在新的主机环境中安装JDK,关闭防火墙.修改selinux.NTP时钟与主机同步.修改hosts.与主机配置ssh免密码登录.保证安装好了perl和py ...
 - 负载均衡集群中的session解决方案
		
前言 在我们给Web站点使用负载均衡之后,必须面临的一个重要问题就是Session的处理办法,无论是PHP.Python.Ruby还是Java,只要使用服务器保存Session,在做负载均衡时都需要考 ...
 - Hadoop(八)Java程序访问HDFS集群中数据块与查看文件系统
		
前言 我们知道HDFS集群中,所有的文件都是存放在DN的数据块中的.那我们该怎么去查看数据块的相关属性的呢?这就是我今天分享的内容了 一.HDFS中数据块概述 1.1.HDFS集群中数据块存放位置 我 ...
 - 负载均衡集群中的session解决方案【转】
		
通常面临的问题 从用户端来解释,就是当一个用户第一次访问被负载均衡代理到后端服务器A并登录后,服务器A上保留了用户的登录信息:当用户再次发送请求时, 根据负载均衡策略可能被代理到后端不同的服务器,例如 ...
 
随机推荐
- XML和JSON两种数据交换格式的比较
			
在web开发领域,主要的数据交换格式有XML和JSON,对于在 Ajax开发中,是选择XML还是JSON,一直存在着争议,个人还是比较倾向于JSON的.一般都输出Json不输出xml,原因就是因为 x ...
 - 性能调优之剖析OutOfMemoryError
			
性能调优之剖析OutOfMemoryError poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询q ...
 - DataTables源码分析(一)
			
DataTables源码分析 写在前面 作为一名常年奋战在java世界中的程序猿,当我接触到现在所谓的前端技术时,内心其实是崩溃的.因为,前端的技术给我的第一个感觉就是"乱",这里 ...
 - LwIP之socket应用--WebServer和Modbus TCP
			
1. 引言 LwIP是嵌入式领域一个流行的以太网协议栈, LwIP开放源码,用C写成非常方便移植,并且支持socket接口,使用者可以集中精力处理应用功能. 本文就是LwIP socket使用的一个小 ...
 - 嵌入javascript脚本的位置
			
JavaScript脚本可以放在HTML文档任何需要的位置.一般来说,可以在<head>与</head>.<body>与</body>标记对之间按需要放 ...
 - 1102: 零起点学算法09——继续练习简单的输入和计算(a-b)
			
1102: 零起点学算法09--继续练习简单的输入和计算(a-b) Time Limit: 1 Sec Memory Limit: 520 MB 64bit IO Format: %lldSub ...
 - 图解函数重载以及arguments
 - ios GCD简单介绍 后台运行~
			
本从实践出发简单说明: 首先,gcd是Grand Central Dispatch的缩写,意为多线程优化技术,是苹果为多核处理优化的技术.使用简单.清晰. 多线程就分同步.异步方法如下: //异步线程 ...
 - STM32学习笔记(二)——串口控制LED
			
开发板芯片:STM32F407ZGT6 PA9-USART1_TX,PA10-USART1_RX; PF9-LED0,PF10-LED1; 一.串口1配置过程(不使用串口中断): 1.使能时钟,包括G ...
 - C#图解教程-方法参数笔记(上)
			
一晃大学四年要过去了,期间乱点了很多技能点, 导致每一项技能都只是处于入门阶段.为了将C#作为我的主要技能,准备恶补相关姿势(知识),通过各种技术论坛的推荐,找到了<C#图解教程>这本书. ...