动态分流

1,370 阅读7分钟

动态分流

文章背景

在业务场景中,流量 是一个服务能否正常运转的重要要素。往大了说,一个服务如若没有流量支撑,那么他也就没有了存在的意义,往小了说,流量大的时候如何控制其不打崩服务,流量不稳定时如何降级处理,流量本身如何做到根据其自身特性分流控制,也是当前架构与优化的核心与方向。

针对分流,亦可以展开更深层的解析。

  • 从大粒度上,流量如何实现整体的分流,在进入服务前,流量实现新旧版本的切流。
  • 在小粒度上,流量进入服务内部之后如何分流进行处理,根据其什么信息进行分流

上述两种分流逻辑,也正好对应了在微服务和分布式整体环境下的两种具体需求(支持但不仅限于)

  • 服务上线后,灰度上线阶段,将某部分既定的流量切到灰度环境进行测试,而其余大部分流量仍然走正式环境。
  • 服务上线后,根据既定要求,将某部分既定的流量切换到相应队列或者相应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==zhfromLang==zh || fromLang==en) && (toLang==zhtoLang==zh || 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)
		}
	}()
}