动态分流
文章背景
在业务场景中,流量 是一个服务能否正常运转的重要要素。往大了说,一个服务如若没有流量支撑,那么他也就没有了存在的意义,往小了说,流量大的时候如何控制其不打崩服务,流量不稳定时如何降级处理,流量本身如何做到根据其自身特性分流控制,也是当前架构与优化的核心与方向。
针对分流,亦可以展开更深层的解析。
- 从大粒度上,流量如何实现整体的分流,在进入服务前,流量实现新旧版本的切流。
- 在小粒度上,流量进入服务内部之后如何分流进行处理,根据其什么信息进行分流
上述两种分流逻辑,也正好对应了在微服务和分布式整体环境下的两种具体需求(支持但不仅限于)
- 服务上线后,灰度上线阶段,将某部分既定的流量切到灰度环境进行测试,而其余大部分流量仍然走正式环境。
- 服务上线后,根据既定要求,将某部分既定的流量切换到相应队列或者相应rpc进行处理。
- 服务上线后,改变相应规则,使对应的流量可以动态变更进行处理(如 需要10%用户进行测试xxx)
动态分流:
实际上,如果采用简单的yml配置化加switch-case 实现分流,会显得服务配置负担严重,服务重启压力大。其次,如果服务内部分流逻辑改动,则需要修改代码重新发布,一方面提高了风险,另一方面也加大了研发人员的维护成本
那么,能不能实现一种满足** 支持各粒度分流,支持配置耦合,动态更改配置,既支持简单通用的分流逻辑动态变更,也满足业务相关的复杂分流逻辑**。并且把其抽象为相应基础模块,抽离相关业务逻辑,以AOP方式作为项目的分流层。
本文将从一个配置臃肿的项目为基点,结合规则引擎 阿波罗 逆波兰式解析法等,构建上述动态分流简单模块。并将在后续模块优化后将其单独抽象成基础模块进行实现。
逆波兰式解析法
说明: 逆波兰式解析法是吧中缀表达式转换成后缀表达式的一种算法,其目的是通过特定的表达式,匹配相应规则,实现分流效果。其表达式相对冗余,在面对相对复杂算法分流的时候会增加项目维护的难度,但却还是可以应用于简单的渠道分流或简单的正则匹配。
算法:中缀表达式转换成后缀表达式
输入:中缀表达式串
输出:后缀表达式串
PROCESS BEGIN:
1.从左往右扫描中缀表达式串s,对于每一个操作数或操作符,执行以下操作;
2.IF (扫描到的s[i]是操作数DATA)
将s[i]添加到输出串中;
3.IF (扫描到的s[i]是开括号’(’)
将s[i]压栈;
4.WHILE (扫描到的s[i]是操作符OP)
IF (栈为空 或 栈顶为’(’ 或 扫描到的操作符优先级比栈顶操作符高)
将s[i]压栈;
BREAK;
ELSE
出栈至输出串中
5.IF (扫描到的s[i]是闭括号’)’)
栈中运算符逐个出栈并输出,直到遇到开括号’(’;
开括号’('出栈并丢弃;
6.返回第1.步
7.WHILE (扫描结束而栈中还有操作符)
操作符出栈并加到输出串中
PROCESS END
以下是关于其的两种实现 java/go ,实现方式略有不同
java:
package cn.hxx.arithmetic.Stack;
import java.util.PriorityQueue;
import java.util.Stack;
/**
* 逆波兰表达式
*
*/
public class Reverse_Polish_notation {
public static void main(String[] args) {
Reverse_Polish_notation r = new Reverse_Polish_notation();
String s = r.reverse_Polish_notation("1-3+4");
System.out.println("逆波兰表达式:"+s);
System.out.println("计算结果:"+r.calculation_Reverse_Polish_notation(s));
}
/**
* 中缀转换到逆波兰表达式
* @param infix 中缀表达式
* @return 后缀表达式
*/
public String reverse_Polish_notation(String infix){
StringBuilder sb = new StringBuilder();
Stack<Character> stack = new Stack<>();
for(int i =0;i<infix.length();i++){
char c = infix.charAt(i);
if(c-'0'<=9 && c-'0'>=0){
sb.append(c);
}else if(c == '('){
stack.add(c);
}else if(c==')'){
//栈中运算符逐个出栈并输出,直到遇到开括号'(';
//开括号'('出栈并丢弃;
while(true){
char c1 = stack.pop();
if(c1 == '('){
break;
}
sb.append(c1);
}
}else {
//碰到操作符
while (true) {
if(stack.size()==0){
stack.add(c);
break;
}
char c1 = stack.peek();
if ( c1 == '(' || judgeOperator(c, c1)) {
stack.add(c);
break;
}else {
sb.append(stack.pop());
}
}
}
}
while (true){
if (stack.size()<=0){
break;
}else {
sb.append(stack.pop());
}
}
return sb.toString();
}
/**
* 判断操作符的优先级
* @param c 扫描的操作符
* @param c1 栈中操作符
* @return
*/
private boolean judgeOperator(char c, char c1) {
if(c =='*'|c=='/'){
return true;
}else{
return false;
}
}
public int calculation_Reverse_Polish_notation(String reverse_Polish_notation){
Stack<Integer> stack = new Stack<>();
for(int i =0;i<reverse_Polish_notation.length();i++){
char c = reverse_Polish_notation.charAt(i);
String string = String.valueOf(c);
if(c-'0'<=9 && c-'0'>=0){
//操作数
stack.add(Integer.parseInt(string));
}else{
//操作符
//弹两个并计算
int num1 = stack.pop();
int num2 = stack.pop();
if(c=='+'){
int total = num1+num2;
stack.add(total);
}else if(c=='-'){
int total = num2-num1;
stack.add(total);
}else if(c=='*'){
int total = num2*num1;
stack.add(total);
}else if(c=='/'){
int total = num2/num1;
stack.add(total);
}
}
}
return stack.pop();
}
}
go:
// 将中缀表达式转为后缀表达式
func (t *TrDistributor) generateRPN(exp string, variables map[string]string) ([]string, error) {
stack := Stack{}
splitedStrs, err := t.convertToStrings(exp, variables)
var datas []string
if err != nil {
return nil, err
}
for i := 0; i < len(splitedStrs); i++ {
tmp := splitedStrs[i]
if t.isOperator(tmp) {
if tmp == "(" ||
stack.IsEmpty() ||
stack.Top().(string) == "(" ||
(tmp != ")" && t.compareOperator(tmp, stack.Top().(string)) == 1) {
stack.Push(tmp)
} else {
if tmp == ")" {
for {
if stack.IsEmpty() {
break
}
if pop := stack.Pop().(string); pop == "(" {
break
} else {
datas = append(datas, pop)
}
}
} else {
for {
pop := stack.Top()
if pop != nil && t.compareOperator(tmp, pop.(string)) != 1 {
datas = append(datas, stack.Pop().(string))
} else {
stack.Push(tmp)
break
}
}
}
}
} else {
datas = append(datas, tmp)
}
}
for {
if pop := stack.Pop(); pop != nil {
datas = append(datas, pop.(string))
} else {
break
}
}
return datas, nil
}
func (t *TrDistributor) convertToStrings(s string, variables map[string]string) ([]string, error) {
sLen := len(s)
var strs []string
for i := 0; i < sLen; i++ {
switch s[i] {
case '(', ')':
strs = append(strs, string(s[i]))
case '&', '|', '=':
if i+1 >= sLen || s[i+1] != s[i] {
return nil, GrammarErr
}
strs = append(strs, string(s[i:i+2]))
i++
case '$':
name, vLen := t.matchValName(s[i:])
if vLen > 0 {
strs = append(strs, variables[name]) // 读取变量值
i += vLen
} else {
return nil, GrammarErr
}
case ' ', '\t':
continue
default:
str, l := t.findString(s[i:])
strs = append(strs, str)
i += l - 1
}
}
return strs, nil
}
func (t *TrDistributor) matchValName(s string) (string, int) {
if len(s) < 2 || s[0] != '$' {
return "", 0
}
for i := 1; i < len(s); i++ {
if !unicode.IsLetter(rune(s[i])) { //变量名只能是字母
if i > 1 {
return s[1:i], i - 1
}
break
}
if i+1 == len(s) {
return s[1:], i
}
}
return "", 0
}
// 匹配字符串,字符串不能包含运算符和空格
func (t *TrDistributor) findString(s string) (string, int) {
for i := 0; i < len(s); i++ {
switch s[i] {
case '(', ')', '&', '|', '=', '$', ' ', '\t':
if i > 0 {
return s[:i], i
} else {
break
}
}
if i+1 == len(s) {
return s, i + 1
}
}
return "", 0
}
// if return 1, o1 > o2.
// if return 0, o1 = 02
// if return -1, o1 < o2
func (t *TrDistributor) compareOperator(o1, o2 string) int {
o1Priority := 0
switch o1 {
case "(", ")":
o1Priority = 3
case "==":
o1Priority = 2
case "&&":
o1Priority = 1
case "||":
o1Priority = 0
}
o2Priority := 0
switch o2 {
case "(", ")":
o1Priority = 3
case "==":
o2Priority = 2
case "&&":
o2Priority = 1
case "||":
o2Priority = 0
}
if o1Priority > o2Priority {
return 1
} else if o1Priority == o2Priority {
return 0
} else {
return -1
}
}
func (t *TrDistributor) isOperator(s string) bool {
switch s {
case "(", ")", "&&", "||", "==":
return true
}
return false
}
规则引擎
显而易见,上述规则引擎存在不可忽视的缺点
- 代码冗余且复杂
- 只能通过相应规则进行匹配 例如:{ name: "hello", condition: "(fromLang==en) && (toLang==en)"}
一般情况下,其可以作为普通业务分流所用到的规则。然而,当业务越来越复杂时,分流的规则势必也会跟着需求变化,假设某需求:需要按照用户粒度进行分流,对id最后位为1的用户取20%走x渠道,而剩余走y渠道。此时,光是书写中缀匹配式便是一件很让人头疼的事情了。那么,为何不重新制定跟用户相关的规则引擎,进行维护呢?此举不仅能将业务相关复杂逻辑与rpn抽离开来便于后续优化,动态分流的变更也会变得更加简单,只需替换规则引擎与相应规则便可以实现分流。
go get github.com/hyperjumptech/grule-rule-engine
调用规则引擎 匹配相应规则方法进行适配
这里简单的调用现有规则引擎进行实现
其中,v3Urls是配置对接算法接口的apins,可以修改值但不能修改键
grls则是具体的规则引擎,目前支持逆波兰式分流RpnFilter 以及 用户粒度分流UserFilter
用户粒度分流的样例方法如下:
rule GreyCheck "if grey < x % then wpsAi_grey" salience 10 {
when
TrDistributor.UserFilter(JobInfo,"123456") < 50
then
TrDistributor.Mq = "wpsAi_v3";
Retract("GreyCheck");
}
以下以新建一个分流规则为例,
首先要在
dataContext := ast.NewDataContext()
temp := TrDistributor{}
dataContext.Add("TrDistributor", &temp)// 1.为grls中的引擎类添加实参(传入方法与变量)
lib := ast.NewKnowledgeLibrary()
ruleBuilder := builder.NewRuleBuilder(lib)
ruleBuilder.BuildRuleFromResource("Test", "0.1.1", pkg.NewBytesResource([]byte(grls)))
kb := lib.NewKnowledgeBaseInstance("Test", "0.1.1")// 2.创建一个新的知识库
eng1 := &engine.GruleEngine{MaxCycle: 50}// 3.规定最大循环次数
eng1.Execute(dataContext, kb)4. 通过传入的实参与分流规则执行
注意点:
- 如需在grls中添加新方法或通过变量传值,其方法与变量都需要是公有的
- 规定最大循环次数不合理时会抛出这个错误(Grule Panicked on Maximum Cycle)
- 在某个条件达成后不再进入此方法 Retract("GreyCheck");
- grls中实际上并没有if else 只有达到不同条件所需要的条件
- 如需往变量中传入字符串,需要先对该字符串冲突的{} 或者"" 进行转义
- 表达式结束必须用; 结尾
动态化
说明: 显而易见,研发人员的维护压力减少了,然而,运维同学又开始头疼了。且看上述简单分流需要配置的文本
v3urls:
{
"g": "",
"Ai": "",
"grey": "",
"v3": "trans",
"test_v3": "trans/test"
}
grls:
rule GreyCheck "if grey < x % then grey" salience 10 {
when
TrDistributor.UserFilter(JobInfo,"123456") < 50
then
TrDistributor.Mq = "v3";
Retract("GreyCheck");
}
倘若通过yml或者config进行配置变更,一方面配置修改的压力大,另一方面,配置修改需要经历服务重启等操作,与分流的初衷相悖。 因此,可以通过现在较为成熟的阿波罗动态配置更新,减少运维同学的压力,使得服务更加自动化。
阿波罗的简单实现如下:
func InitApolloClient(appConfig env.AppConfig) error {
if err := apolloclient.InitCustomConfig(func() (config *env.AppConfig, e error) {
//todo: 处理配置默认值
return &appConfig, nil
}); err != nil {
return err
}
if err := apolloclient.Start(""); err != nil {
return err
}
return nil
}
监听阿波罗,获取相应值
func ListenApolloConfig() {
// 启动apollo客户端
apolloConfig := new(env.AppConfig)
if err := config.UnmarshalKey("apollo", &apolloConfig); err != nil {
panic(err)
}
apolloclient.InitCustomConfig(func() (*env.AppConfig, error) {
return apolloConfig, nil
})
if err := apolloclient.Start(""); err != nil {
panic("start error, err: " + err.Error())
}
// 添加监听
listerer := &config.CustomChangeListener{}
storage.AddChangeListener(listerer)
// 当配置改变触发
go func() {
for {
listerer.W.Add(1)
listerer.W.Wait()
// 重新加载分发配置
ps := manager.GetPublishManager()
ps.ReloadV3urls(listerer.Event.Changes)
}
}()
}