├── requirements.txt ├── MANIFEST.in ├── image ├── logo.jpg ├── regal_img.png ├── regal-logo.jpg ├── regal_imag2.png ├── regal_imag3.png └── regal_img ├── regal ├── __init__.py ├── format.py ├── check_interface.py ├── tools.py ├── control.py └── grouping.py ├── .travis.yml ├── CHANGLOGS ├── example.py ├── .gitignore ├── LICENSE ├── setup.py ├── tests.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | six==1.11.0 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md -------------------------------------------------------------------------------- /image/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boylegu/regal/HEAD/image/logo.jpg -------------------------------------------------------------------------------- /image/regal_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boylegu/regal/HEAD/image/regal_img.png -------------------------------------------------------------------------------- /regal/__init__.py: -------------------------------------------------------------------------------- 1 | from regal.control import BaseInfo 2 | 3 | __all__ = ['BaseInfo'] 4 | -------------------------------------------------------------------------------- /image/regal-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boylegu/regal/HEAD/image/regal-logo.jpg -------------------------------------------------------------------------------- /image/regal_imag2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boylegu/regal/HEAD/image/regal_imag2.png -------------------------------------------------------------------------------- /image/regal_imag3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boylegu/regal/HEAD/image/regal_imag3.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7 8 | # command to install dependencies 9 | install: 10 | 11 | - "pip install -r requirements.txt" 12 | - "pip install coveralls" 13 | 14 | # command to run tests 15 | script: coverage run --source=regal setup.py test 16 | 17 | after_scuccess: "coveralls" -------------------------------------------------------------------------------- /CHANGLOGS: -------------------------------------------------------------------------------- 1 | 2 | Changelog for Regal 3 | ================= 4 | 5 | This file lists the changes in each regal version. 6 | 7 | 8 | 1.3 9 | ----- 10 | 11 | - 新增对six 1.11的支持 12 | 13 | - 新增tools模块,支持打散 14 | 15 | 1.1 16 | ----- 17 | 18 | - 新增对Python3的支持 19 | 20 | - 修复小部分引起异常问题 21 | 22 | 23 | 1.0 24 | ----- 25 | 26 | - 提供发布策略,动态智能分流 27 | 28 | - 支持多版本分组和优先级 29 | 30 | - 提供基本的数据格式化 -------------------------------------------------------------------------------- /regal/format.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from six.moves import zip 3 | 4 | 5 | class Format(object): 6 | """ 7 | 专用于处理各种数据的格式化,目前还比较简单;之后有时间会加入json的支持-_- 8 | """ 9 | 10 | def __init__(self, result_list): 11 | self.result = result_list 12 | 13 | def iter_dict(self): 14 | for i in zip(self.result): 15 | yield { 16 | i[0][0]: [','.join(ip) for ip in i[0][1]] 17 | } 18 | -------------------------------------------------------------------------------- /regal/check_interface.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import abc 3 | 4 | from six import with_metaclass 5 | 6 | 7 | class AlgorithmABC(with_metaclass(abc.ABCMeta, object)): 8 | """ 9 | 实现了一套简单的虚拟接口检查类, 主要用于检测'发布算法器'的各项接口是否存在. 10 | """ 11 | 12 | @classmethod 13 | def __subclasshook__(cls, subclass): 14 | """ 15 | 重写了instance方法, 用于实现一个虚拟接口; 16 | :param subclass: 被检测的接口类 17 | :return: 18 | 19 | 其中__mro__方法主要是把所有的subclass和base类的所有属性都遍历出来,并放在一个容器中; 20 | 然后通过定义好的抽象接口属性name,与其进行比对,最终返回结果. 21 | """ 22 | if cls is AlgorithmABC: 23 | methods = ("initialize", "calculate", "final") 24 | all_attribute_dict = [all_attribute.__dict__ for all_attribute in subclass.__mro__][0] 25 | if all(method in all_attribute_dict for method in methods): 26 | return True 27 | return NotImplemented 28 | -------------------------------------------------------------------------------- /regal/tools.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from itertools import groupby 3 | from collections import Counter 4 | 5 | 6 | def scatters(source): 7 | source_items = ''.join(sorted(source)) 8 | list_ = [''.join(i.split()) for i in source_items] 9 | result = [] 10 | 11 | def redex(list_): 12 | scatter = [i[0] for i in groupby(list_)] 13 | if len(scatter) > 1: 14 | result.extend(scatter) 15 | c1 = Counter(list_) 16 | c2 = Counter(scatter) 17 | diff = c1 - c2 18 | intersection = list(diff.elements()) 19 | if (len(set(intersection)) == 1) or not intersection: 20 | result.extend(intersection) 21 | return result 22 | else: 23 | return redex(intersection) 24 | 25 | redex(list_) 26 | return result 27 | 28 | 29 | if __name__ == "__main__": 30 | print(scatters('CBBCCCB')) # ===> ['B', 'C', 'C', 'B', 'C', 'B', 'C'] -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # regal v1.1 example 3 | from regal import BaseInfo 4 | 5 | 6 | ab = BaseInfo( 7 | version_host={ 8 | 'test-app-1.0.0-SNAPSHOT-201512191829.war': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4,5.1.1.1,6.2.2.2,7.3.3.3,8.4.4.4'}, 9 | combine=4, # 每组四台 10 | schedule=3 # groupA组 分三台 11 | ) 12 | 13 | cc = ab.grouping() 14 | print(cc.result) 15 | for i in cc.iter_dict(): 16 | print(i) 17 | 18 | 19 | # 多版本支持 20 | ab = BaseInfo( 21 | version_host={'ver1': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4', 22 | 'ver2': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4', 23 | 'ver3': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4', 24 | 'ver4': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4', }, 25 | combine=3, 26 | # schedule=2 # groupA组 分三台 27 | ) 28 | 29 | cc = ab.grouping(priority_name='ver2') # priority_name 多版本的情况下可以作为优先级策略 30 | print(cc.result) 31 | for i in cc.iter_dict(): 32 | print(i) 33 | 34 | 35 | # 以上都只是例子, 事实上服务器可以更多,甚至可以非常多; 请随意~~ 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # PycharmIDE tempfile 61 | .idea 62 | 63 | # image & logo 64 | image/*.xml 65 | image/*.pxm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | "regal" 2 | 3 | ============================ 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2016 Boyle Gu 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | try: 3 | from setuptools import setup 4 | except ImportError: 5 | from distutils.core import setup 6 | 7 | 8 | __version_info__ = (1, 3) 9 | __version__ = '.'.join([str(v) for v in __version_info__]) 10 | 11 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 12 | README = readme.read() 13 | 14 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 15 | 16 | setup( 17 | name='regal', 18 | version=__version__, 19 | packages=['regal'], 20 | include_package_data=True, 21 | license='MIT License', 22 | description='A/B Testing or publish smart grouping engine.', 23 | long_description=open('README.md', 'r').read(), 24 | long_description_content_type="text/markdown", 25 | url='https://github.com/boylegu/regal', 26 | author='BoyleGu', 27 | author_email='gubaoer@hotmail.com', 28 | install_requires=['six==1.11.0'], 29 | classifiers=[ 30 | 'Development Status :: 5 - Production/Stable', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: Implementation :: CPython', 38 | 'Topic :: Internet :: WWW/HTTP', 39 | 'Topic :: Software Development :: Libraries', 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from regal import BaseInfo 4 | from regal.grouping import GroupAlgorithm 5 | from regal.check_interface import AlgorithmABC 6 | 7 | 8 | # Run Method: python -m unittest -v tests.py 9 | class TestBaseInfoInitial(TestCase): 10 | def test_empty_info(self): 11 | ab = BaseInfo('', '', '') 12 | with self.assertRaises(AttributeError): 13 | ab.grouping() 14 | 15 | def test_empty_info_version_host_isdict(self): 16 | ab = BaseInfo({}, '', '') 17 | self.assertIsNotNone(ab.grouping()) 18 | 19 | def test_info_errortype(self): 20 | ab = BaseInfo({}, '1', 'sds') 21 | self.assertIsNotNone(ab.grouping()) 22 | 23 | 24 | class TestGroupingResult(TestCase): 25 | ver = { 26 | 'ver1': '1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4,5.1.1.1,6.2.2.2,7.3.3.3,8.4.4.4'} 27 | combine_num = 4 28 | 29 | def test_combine_num(self): 30 | ab = BaseInfo( 31 | self.ver, 32 | self.combine_num 33 | ) 34 | instance_combine_num = ab.grouping().result[0][1] 35 | self.assertEqual(len(instance_combine_num[1:-1][0]), self.combine_num) 36 | 37 | def test_schedule_num(self): 38 | schedule_num = 2 39 | ab = BaseInfo(self.ver, self.combine_num, schedule_num) 40 | instance_combine_num = ab.grouping().result[0][1] 41 | self.assertEqual(len(instance_combine_num[0][0].split(',')), schedule_num) 42 | 43 | 44 | class TestInstance(TestCase): 45 | def test_algorithm_instance(self): 46 | self.assertIsInstance(GroupAlgorithm(), AlgorithmABC) 47 | -------------------------------------------------------------------------------- /regal/control.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from six import iteritems 3 | 4 | from regal.check_interface import AlgorithmABC 5 | from regal.grouping import GroupAlgorithm 6 | from regal.format import Format 7 | 8 | 9 | class BaseInfo(object): 10 | """ 11 | 收集初始化信息的类 12 | """ 13 | 14 | def __init__(self, version_host, combine=None, schedule=None): 15 | self.host_version = version_host 16 | self.schedule = 1 if not schedule else schedule 17 | self.combine = 1 if not combine else combine 18 | 19 | def __add_info(self): 20 | try: 21 | version_group = [(version, [hostname]) for version, hostname in iteritems(self.host_version)] 22 | except: 23 | raise AttributeError("ERROR! The information is wrong") 24 | return version_group 25 | 26 | def grouping(self, priority_name=None): 27 | control = Manager( 28 | base_info=self.__add_info(), 29 | grouping_algorithm=GroupAlgorithm(), # Bridge Pattern 30 | ) 31 | return control.grouping(self.combine, self.schedule, priority_name) 32 | 33 | 34 | class Manager(object): 35 | """ 36 | 一个class hierarchy, 作为具体分组实现的方式 37 | """ 38 | 39 | def __init__(self, base_info, grouping_algorithm): 40 | self.base_info = base_info 41 | if not isinstance(grouping_algorithm, AlgorithmABC): 42 | raise TypeError( 43 | "Expected object of type Algorithm, got {}".format(type(grouping_algorithm).__name__)) 44 | self.__algorithm = grouping_algorithm 45 | 46 | def grouping(self, combine, schedule, priority_name): 47 | self.__algorithm.initialize(self.base_info) 48 | self.__algorithm.calculate(combine, schedule) 49 | result = self.__algorithm.final(priority_name) 50 | return Format(result) 51 | -------------------------------------------------------------------------------- /regal/grouping.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from operator import contains 3 | 4 | from six.moves import xrange 5 | 6 | 7 | class GroupAlgorithm(object): 8 | """ 9 | 一个abstraction class,主要实现了分流算法的接口,不过代码应该还可以再一次深度优化; 10 | 11 | """ 12 | base_hostlist = None 13 | host_list = None 14 | 15 | @classmethod 16 | def recursive_grouping(cls, hosts, combine, hostindex, init_host, base_hostlist): 17 | 18 | baselist = base_hostlist 19 | 20 | def grouping(hosts, combine, hostindex, init_host, init_n=0): 21 | try: 22 | f_count = init_n + 1 # 记录创建子列表的次数 23 | baselist[hostindex][1][0] = [init_host] 24 | baselist[hostindex][1].append(list()) 25 | for i in xrange(combine): 26 | baselist[hostindex][1][init_n + 1].append(hosts.pop()) 27 | except IndexError: 28 | return 0 29 | else: 30 | return grouping(hosts, combine, hostindex, init_host, f_count) 31 | 32 | return grouping(hosts, combine, hostindex, init_host, init_n=0) 33 | 34 | def initialize(self, hostinfo): 35 | self.base_hostlist = list() 36 | self.host_list = [(i[0], ','.join(i[1]).split(',')) for i in hostinfo] 37 | return 38 | 39 | def calculate(self, combine, schedule): 40 | for infoindex, info in enumerate(self.host_list): 41 | self.base_hostlist.append((info[0], [[]])) 42 | # print ','.join(info[1][:2]) 43 | hosts = info[1][schedule:] 44 | hosts.reverse() 45 | GroupAlgorithm.recursive_grouping( 46 | hosts=hosts, combine=combine, hostindex=infoindex, 47 | init_host=','.join(info[1][:schedule]), base_hostlist=self.base_hostlist) 48 | 49 | if not self.base_hostlist[infoindex][1][-1]: 50 | self.base_hostlist[infoindex][1].pop(-1) 51 | return 52 | 53 | def final(self, priority_name): 54 | base_hostlist = self.base_hostlist 55 | if priority_name: 56 | base_hostlist = sorted( 57 | base_hostlist, key=lambda x: contains(x[0], priority_name), reverse=True) 58 | return base_hostlist 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Regal 2 | ===== 3 | 4 | [![pyversions](https://img.shields.io/badge/python-2.6%2C2.7%2C3.4%2C3.5-blue.svg)]() 5 | [![ver](https://img.shields.io/badge/release-v1.3-red.svg)]() 6 | [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)]() 7 | [![coverage](https://img.shields.io/badge/coverage-92%25-yellowgreen.svg)]() 8 | [![Build Status](https://travis-ci.org/boylegu/regal.svg?branch=master)](https://travis-ci.org/boylegu/regal) 9 | [![Code Climate](https://codeclimate.com/github/boylegu/regal/badges/gpa.svg)](https://codeclimate.com/github/boylegu/regal) 10 | 11 | 用于"灰度发布"或 A/B Testing的智能分组引擎 12 | 13 |

14 | sanic_vue 15 | 16 | ## Regal能做什么? 17 | 举个最简单的例子,比如需要针对一个版本进行灰度发布,而这一版本对应的可能是一大堆服务器集群, 如下图: 18 | 19 | ![Markdown](https://github.com/boylegu/regal/blob/master/image/regal_img.png?raw=true) 20 | 21 | 就像图中描述的一样,无论你的服务器是多还是少,尤其很多中小型企业在进行灰度发布时,通常会遇到所制定的分流策略在实际的技术或开发中如何去实现,是机器直接写死? 22 | 23 | 因此让``Regal智能分组引擎``直接介入,让它来根据你的策略提前进行动态地分组分流。 24 | 在这里,我再举一个简单的例子,方便大家能够更清楚的明白Regal的主要工作: 25 | 26 | 假设有一个版本A,需要针对六台机器进行发布 27 | 28 | ![Markdown](https://github.com/boylegu/regal/blob/master/image/regal_imag2.png?raw=true) 29 | 30 | 现在应该已经了解Regal到底是什么干货了吧,当然了,上面的例子是服务器非常少的情况,实际情况中,所面对的服务器集群是非常多,这个时候可以通过提供的``combine``和``schedule``两个API进行策略调整。详情可以见下文的``使用介绍`` 31 | 32 | - Feature: 33 | 34 | 1. 提供发布策略,动态智能分流 35 | 2. 支持多版本分组和优先级 36 | 3. 数据格式化 37 | 4. 同时兼容Python2.5以上和Python3以上的版本(建议使用Python2.7+或者Python3.5以后的版本) 38 | 39 | 40 | ## 安装和使用 41 | 42 | ### 安装 43 | 44 | - `` pip install regal `` 45 | 46 | ### 使用说明 47 | 48 | - 单个版本场景 49 | 50 | ``` 51 | In [1]: from regal import BaseInfo 52 | 53 | 54 | # 初始化信息,请注意一下格式 55 | In [6]: ab = BaseInfo( 56 | version_host={'app-test-version1.0':'10.1.1.1,10.1.1.2,10.1.1.3,10.1.1.4,10.1.1.1.5'}, 57 | combine=2 # combine 希望以每组多少台服务器作为一组,进行用户群B的分流 58 | # 在这个例子中为2台 59 | # 默认:每组1台 60 | ) 61 | 62 | # grouping() 进行分组 63 | In [11]: smart_grouping = ab.grouping() 64 | 65 | 66 | # result属性 进行分组后的返回结果 67 | In [12]: smart_grouping.result 68 | Out[12]: 69 | [('app-test-version1.0', 70 | [['10.1.1.1'], ['10.1.1.2', '10.1.1.3'], ['10.1.1.4', '10.1.1.1.5']])] 71 | ``` 72 | 根据你的策略设置,会得到一个数据结构,我们来观察一下: 73 | 74 | ![Markdown](https://github.com/boylegu/regal/blob/master/image/regal_imag3.png?raw=true) 75 | 76 | 再看一个例子 77 | 78 | ``` 79 | In [7]: ab = BaseInfo( 80 | version_host={'app-test-version1.0':'10.1.1.1,10.1.1.2,10.1.1.3,10.1.1.4,10.1.1.5'}, 81 | combine=3, 82 | schedule=2) 83 | 84 | In [10]: ab.grouping().result 85 | Out[10]: 86 | [('app-test-version1.0', 87 | [['10.1.1.1,10.1.1.2'], ['10.1.1.3', '10.1.1.4', '10.1.1.5']])] 88 | 89 | ``` 90 | 91 | - 多版本场景 92 | 93 | ``` 94 | In [17]: ab = BaseInfo( 95 | ....: version_host={ 96 | ....: 'app-test-version1.0': '10.1.1.1,10.1.1.2,10.1.1.3,10.1.1.1.4,10.1.1.5', 97 | ....: 'app-test-version2.0': '10.1.1.9,10.1.1.8,10.1.1.7,10.1.1.6'}, 98 | ....: combine=3, 99 | ....: schedule=2 100 | ....: ) 101 | 102 | In [20]: ab.grouping().result 103 | Out[20]: 104 | [('app-test-version2.0', [['10.1.1.9,10.1.1.8'], ['10.1.1.7', '10.1.1.6']]), 105 | ('app-test-version1.0', 106 | [['10.1.1.1,10.1.1.2'], ['10.1.1.3', '10.1.1.1.4', '10.1.1.5']])] 107 | 108 | 109 | # grouping()方法还提供了priority_name参数,当需要在多版本发布的时候,设置优先级,指定你需要优先发布的'版本名' 110 | In [22]: smart_grouping = ab.grouping(priority_name='app-test-version1.0') 111 | 112 | In [23]: smart_grouping.result 113 | Out[23]: 114 | [('app-test-version1.0', 115 | [['10.1.1.1,10.1.1.2'], ['10.1.1.3', '10.1.1.1.4', '10.1.1.5']]), 116 | ('app-test-version2.0', [['10.1.1.9,10.1.1.8'], ['10.1.1.7', '10.1.1.6']])] 117 | 118 | # 提供一个简易的API,可以让结果返回的更简洁 119 | In [16]: for i in smart_grouping.iter_dict(): 120 | print i 121 | ....: 122 | {'app-test-version1.0': ['10.1.1.1', '10.1.1.2,10.1.1.3', '10.1.1.4,10.1.1.1.5']} 123 | 124 | ``` 125 | 126 | ## Demo 127 | 128 | - 你也可以通过 `` git clone https://github.com/boylegu/regal/ `` 129 | 130 | - `` cd regal/ `` 131 | 132 | - 参考`` example.py `` 133 | 134 | 135 | ## 分流分组之后? 136 | 137 | Regal本身只是一个分组引擎,因此它并不承担直接发布的作用,但是通过Regal分组之后,你所得到数据,是非常容易和其他可以用来发布的组件进行配合;下面是我的一些建议和指导。 138 | 139 | ``` 140 | versionA: 141 | 142 | (第一组) groupA ip...... 用户群A 143 | (第二组) groupB1 ip...... __ 144 | (第三组) groupB2 ip...... | 145 | (第四组) groupB3 ip...... | -- 用户群B 146 | ...... --| 147 | ``` 148 | 149 | - 关于发布 150 | 151 | 分组之后,每一组的所有机器可以看作一个整体,扔进发布组件,进行'组内并发' 152 | 153 | 你可以把每一组直接放在ansible、saltstack、pssh或异步IO框架等等进行发布; 154 | 155 | 甚至你也可以和前端nginx+lua进行组合; 156 | 157 | - 关于停止发布 158 | 159 | 每组进行发布,一旦出现异常,你可以利用发布组件,或者你自己写一套异常抓取工具来停止发布,这个时候就不会再针对剩下的组进行发布操作了。 160 | 161 | - 关于回滚 162 | 163 | 把回滚也看作一种发布,就不多说了 164 | 165 | ## 作者 166 | 167 | - 顾鲍尔 (Boyle Gu) 168 | 169 | ## 技术交流与支持 170 | 171 | 有任何问题、建议可以通过Github; 172 | 173 | 也可以直接加入讨论群 QQ:315308272 与我进行交流 174 | 175 | 176 | ## Darwin's finches 177 | 178 | ![Markdown](https://github.com/boylegu/regal/blob/master/image/regal-logo.jpg?raw=true) 179 | 180 | 第一次在Mac上绘图,这就当做本项目的吉祥物吧~ 181 | 182 | 人类的创造从来没有离开大自然带给我们的启发,而无论是灰度发布,还是A/B Testing,早在千年以前,大自然早有绝佳的解决方案。因此我以‘Darwin's finches’作为原型,手工绘制了这张图,向伟大的大自然和达尔文《物种起源》致敬。 183 | 184 | > Author: 顾鲍尔 185 | > Date: 2015.12.23 绘 186 | 187 | 188 | -------------------------------------------------------------------------------- /image/regal_img: -------------------------------------------------------------------------------- 1 | 2 | 3 |
版本A
[Not supported by viewer]
10.32.0.1,10.32.0.2,10.32.0.3,10.32.0.4,10.32.0.5,10.32.0.6
[Not supported by viewer]
初始化
[Not supported by viewer]
版本A
[Not supported by viewer]
让Regal对灰度发布前
进行分组
[Not supported by viewer]
10.32.0.1
[Not supported by viewer]
10.32.0.2,10.32.0.3
[Not supported by viewer]
10.32.0.4,10.32.0.5
[Not supported by viewer]
10.32.0.6
[Not supported by viewer]
通过Regal提供的API,‘combine’来指定由集群中有多少台服务器合并为一组,在此例子中指定的是 2
[Not supported by viewer]
一个或多个服务器集群
[Not supported by viewer]
GroupA
[Not supported by viewer]
GroupB1
[Not supported by viewer]
GroupB2
[Not supported by viewer]
GroupB3
[Not supported by viewer]
10.32.0.1
[Not supported by viewer]
GroupA
[Not supported by viewer]
10.32.0.2,10.32.0.3,10.32.0.4
[Not supported by viewer]
GroupB1
[Not supported by viewer]
10.32.0.5,10.32.0.6
[Not supported by viewer]
GroupB2
[Not supported by viewer]
或者如果combine为 3,那么每组包含三台机器,最后一组
[Not supported by viewer]
版本A
[Not supported by viewer]
分流
[Not supported by viewer]
通过以下Regal 智能分组发布引擎
[Not supported by viewer]
批注
[Not supported by viewer]
第一组作为灰度发布中的第一批
用户,也就是Test A。第一组发布
的机器数默认是1;你可以通
Regal提供的API,‘schedule
来更改这一行为。

[Not supported by viewer]
A用户群
[Not supported by viewer]
B用户群
[Not supported by viewer]
批注
[Not supported by viewer]
由于总的机器的数量已经快
被分组完了,那最后一组机
器的数量自动作为一组。
[Not supported by viewer]
1
[Not supported by viewer]
2
[Not supported by viewer]
3
[Not supported by viewer]
经过Regal 引擎的策略分组后 
[Not supported by viewer]
--------------------------------------------------------------------------------