基于zookeeper的配置管理中心

作者:李世海
链接:https://www.jianshu.com/p/01388f06e75d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

zookeeper是什么:

https://zookeeper.apache.org/

zookeeper的用途

  • 数据发布与订阅(配置中心)
  • 负载均衡
  • 命名服务(Naming Service)
  • 分布式通知/协调
  • 集群管理与Master选举
  • 分布式锁
  • 分布式队列

为什么要实现这样的一个实例:

当在一个集群规模的环境中,多台同类型的应用使用同样的配置文件,为了避免登陆每台机器修改配置,为了减少人为的修改导致配置不一致,为了实现配置文件的统一管理、版本控制,那么就有必要实现一个配置管理中心的应用。

实例用到的软件

python (3.4.2)
zookeeper (3.4.8)
kazoo (2.2.1)
tornado (4.3)

配置管理中心实现了哪些功能

0. 标准的key value的配置文件几个
1. 基于web页面的配置文件的增删改查
2. 同一个app下多个配置文件
3. 版本管理,当前版本,历史版本管理
4. 数据持久化保存
5. 非标准的key value配置文件的管理
6. 客户端watcher的实现

配置管理中心的原理

 

zookeeper节点的设计

/conf
  |
  |-------appid
  |         |
  |         |------conf_name
                    |
                    |-----key1
                    |
                    |-----key2
                    |
            |------version
                    |
                    |---history
                    |
                    |---current
  • conf: zookeeper的跟节点
  • appid: app的名称
  • conf_name: 配置文件名称
  • key: 配置文件中的key,对应存储的值为value
  • version: 版本控制节点
  • history: 历史版本
  • current: 当前版本
  • 例如想管理一个名称叫app1的应用,它的配置文件为app1.conf
  • app1.conf的内容为:
  • vim app1.conf
  • host=127.0.0.1
  • username='hans'
  • 那么对应的节点为:
  • /conf/app1/app1.conf/host

web界面展示

显示

 

创建

create.png

代码分析

主要代码

handler.py
import time
import os
import shutil
from os import path as os_path
from tornado.web import RequestHandler
from tornado.options import options
from tornado.web import HTTPError
from kazoo.exceptions import NodeExistsError


class CreateHandler(RequestHandler):
    @staticmethod
    def persistence_conf(filename, content):
        if not os.path.exists(os.path.dirname(filename)):
            os.makedirs(os.path.dirname(filename))
        with open(filename, 'w') as f:
            f.write(content)

    @staticmethod
    def copy_file(src, dst):
        if os.path.isdir(src) and os.path.isdir(dst):
            for f in os.listdir(src):
                if os.path.isfile(os_path.join(src, f)):
                    shutil.copy(os_path.join(src, f), os_path.join(dst, f))

    def write_history(self, node, version):
        self.application.zk.ensure_path(os_path.join(node, 'history'))
        try:
            node = os_path.join(node, 'history')
            self.application.zk.create(os_path.join(node, version), b'')
        except NodeExistsError:
            raise HTTPError(500, reason='{0} is already exist !')
        except Exception as e:
            raise HTTPError(500, reason=str(e))

    def write_current(self, node, version):
        self.application.zk.ensure_path(os_path.join(node, 'current'))
        try:
            node = os_path.join(node, 'current')
            self.application.zk.create(os_path.join(node, 'version'), version.encode())
        except NodeExistsError:
            self.application.zk.set(os_path.join(node, 'version'), version.encode())
        except Exception as e:
            raise HTTPError(500, reason=str(e))

    def get(self):
        self.render('create.html')

    def post(self):
        appid = self.get_argument('appid')
        conf_name = self.get_argument('conf_name')
        content = self.get_argument('content')

        # 创建节点/conf/emnp/jdbc.properties
        node = os_path.join(options.root, appid, conf_name)
        self.application.zk.ensure_path(node)
        # 创建子节点
        for line in content.split('\n'):
            try:
                n = line.split('=')
                k, v = n[0].strip(), n[1].strip()
                self.application.zk.create(os_path.join(node, k), v.encode())
            except IndexError:
                pass
            except NodeExistsError:
                # 更新节点
                self.application.zk.set(os_path.join(node, k), v.encode())
            except Exception as e:
                raise HTTPError(500, reason=str(e))

        # 持久化数据
        version = str(time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())))
        latest_dir = ''
        new_dir = '/'.join([options.workspace, options.root, appid, version])

        path = '/'.join([options.workspace, options.root, appid])
        try:
            dirs = [d for d in os.listdir(path)]
            if dirs:
                latest_dir = os_path.join(path, max(dirs))
                os.makedirs(new_dir)
                CreateHandler.copy_file(latest_dir, new_dir)
        except FileNotFoundError:
            pass

        file_path = '/'.join([new_dir, conf_name])
        CreateHandler.persistence_conf(file_path, content)
        # 写history
        node = '/'.join([options.root, appid, 'version'])
        self.write_history(node, version)
        # 写current
        self.write_current(node, version)

        self.redirect('/')


class IndexHandler(RequestHandler):
    def get(self):
        """
        test = {
            "app1":{
                "current_version":"2016",
                "history_version":['2015','2016'],
                "conf_files":["app1.conf","app2.conf","app2.conf"]
            },
            "app2": {
                "current_version": "1900",
                "history_version": ['1901', '1900'],
                "conf_files": ["cc1.conf", "cc2.conf", "cc3.conf"]
            }
        }
        """

        data = {}
        node = os_path.join(options.root)
        appids = self.application.zk.get_children(node)
        conf_files = None
        for app in appids:
            conf_files = self.application.zk.get_children(os_path.join(node, app))
            conf_files.remove('version')
            current_version, _ = self.application.zk.get(os_path.join(node, app, 'version', 'current', 'version'))
            history_version = self.application.zk.get_children(os_path.join(node, app, 'version', 'history'))
            data[app] = {
                "current_version": current_version.decode('utf-8'),
                "history_version": history_version,
                "conf_files": conf_files,
            }

        self.render('index.html', apps=data)


class ShowHandler(RequestHandler):
    def get(self, *args, **kwargs):
        content = ''
        data = self.get_argument('data')
        appid,conf_name,current_version= data.split('(')

        path = '/'.join([options.workspace,options.root,appid,current_version,conf_name])
        with open(path) as f:
            content = f.read()

        conf_content = {
            'appid':appid,
            'conf_name':conf_name,
            'content':content
        }
        self.render('show.html',conf_content=conf_content)

class EditHandler(RequestHandler):
    def get(self, *args, **kwargs):
        content = ''
        data = self.get_argument('data')
        appid, conf_name, current_version = data.split('(')

        path = '/'.join([options.workspace, options.root, appid, current_version, conf_name])
        with open(path) as f:
            content = f.read()

        conf_content = {
            'appid': appid,
            'conf_name': conf_name,
            'content': content
        }
        self.render('edit.html', conf_content=conf_content)

class DeleteHandler(RequestHandler):

    def get(self, *args, **kwargs):
        content = ''
        data = self.get_argument('data')
        appid, conf_name, current_version = data.split('(')

        node = os_path.join(options.root,appid,conf_name)
        try:
            self.application.zk.delete(node,recursive=True)
        except Exception as e:
            raise HTTPError(500, reason=str(e))

        self.redirect('/')

github地址:https://github.com/hansdcr/config_center

TODO

  1. 非标准配置文件的管理
  2. 非标准配置管理的watcher客户端

 

同类型的开源方案

1. 王阿晶,邹仕洪: [基于ZooKeeper的配置信息存储方案的设计与实现](http://wenku.baidu.com/view/ee86ca90daef5ef7ba0d3c7d.html)
2. 淘宝diamod实现:[http://code.taobao.org/p/diamond/src/](http://code.taobao.org/p/diamond/src/), 2012
3. disconf github: [https://github.com/knightliao/disconf](https://github.com/knightliao/disconf), 2014
4. [百度BJF配置中心](http://wiki.babel.baidu.com/twiki/bin/view/Main/CAP-CC#%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%831.0%E5%BF%AB%E9%80%9F%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97.pptx), 2014




posted @ 2019-11-22 17:48  逐梦客!  阅读(663)  评论(0)    收藏  举报