├── 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 | []() 5 | []() 6 | []() 7 | []() 8 | [](https://travis-ci.org/boylegu/regal) 9 | [](https://codeclimate.com/github/boylegu/regal) 10 | 11 | 用于"灰度发布"或 A/B Testing的智能分组引擎 12 | 13 |
14 |
15 |
16 | ## Regal能做什么?
17 | 举个最简单的例子,比如需要针对一个版本进行灰度发布,而这一版本对应的可能是一大堆服务器集群, 如下图:
18 |
19 | 
20 |
21 | 就像图中描述的一样,无论你的服务器是多还是少,尤其很多中小型企业在进行灰度发布时,通常会遇到所制定的分流策略在实际的技术或开发中如何去实现,是机器直接写死?
22 |
23 | 因此让``Regal智能分组引擎``直接介入,让它来根据你的策略提前进行动态地分组分流。
24 | 在这里,我再举一个简单的例子,方便大家能够更清楚的明白Regal的主要工作:
25 |
26 | 假设有一个版本A,需要针对六台机器进行发布
27 |
28 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------