Scrum中大项目管理

团队使用Scrum 已经有半年多了, 基本形成了一周一个迭代周期的正常开发节奏,团队的开发状态也进入了正规;但是在Scrum的迭代中,时常会出现一些比较大项目需求,这种大项目工期长,时常会跨团队/部门配合,因此在迭代中会出现很多问题,难以管理。

这篇文章是我们团队在周会上专门讨论大项目管理时的总结。

1. Scrum迭代管理

在Scrum迭代中有是哪个重要的节点:

  • Sprint需求会

    在需求会上,主要是产品经理(PO)向技术团队阐述产品功能,在这个过程中开发人员会参与到产品细节的讨论中;这个讨论过程不仅能让开发更深入的理解产品的意义和细节,同时也可以提出自己对产品不同的见解,也可以直接挑战PO。最终会完成Story评审,对Stroy进行估点(评定Point),对各个Story确定优先级。需求会一般持续一个半小时以内。

  • Sprint计划会

    计划会(planning)上会review一下上个迭代遗留下来的story和task,评估剩余的时间;之后会评估在需求会上评审通过的story,划分task,给出实现所需时间。之后每个人会主动领取task。目前我们每个迭代按照每天6小时计算开发时间,剩余的时间作为运营时间。

  • 产品交付

    对应系统功能就是功能上线,最终交付给PO。

2. 大项目管理

大项目具有以下特点:

  • 功能复杂,需要有技术设计来厘清
  • 开发量大,项目工期长;
  • Story难以拆小,或者拆小之后的Story没有意义;
  • 可能跨团队/部门的合作。

针对需要跨团队/部门合作的大项目,因为其他团队/部门的开发并不一定使用Scrum的迭代管理方式,即使他们使用相同的Scrum迭代周期,所以针对这种大项目更适合是使用瀑布式管理方式。大项目管理主要做好一下几个关键点,在实施过程中也就不会出现大的问题:

  • 需求评审

    大项目说明需求本身就很大,因此在需求评审阶段把控好需求的方向和细节,包括需求内容和背景、确定干系方、初步技术可行性聘雇和初步的分线评估。

  • 技术设计

  • 分工排期

    这个分工排期需要确定各个干系方的分工,每个干系方不同阶段的时间节点,包括PO需要提供到的物料(交互、视觉设计等),必要的情况下,需要干系方定期召开例会以同步进度和问题。

  • 上线方案和时间点

    大项目时常涉及到的较多的项目修改,需要做好上线方案和回滚方案的评审。上线方案需要确定需要上线的项目、配置、数据表(库)及其他运维项,这个上线方案需要分步骤验证,避免出现上线步骤接口或功能的兼容性问题;回滚方案是针对上线过程中一单某个步骤出现问题该如何回滚,也需要针对回滚方案分步骤验证。

3. Scrum中的大项目管理

大项目放在Scrum中管理,有种把大象放到冰箱里,不过这次的步骤却不是打开冰箱把大象放进去。不过大项目可以借助Scrum中迭代的方式进行管理。

  • 需求评审放在Scrum迭代需求评审会上进行
  • Scrum计划会上划分task,如技术评审task,与其他团队/部门沟通task,开发task,测试task和上线task等。
  • 在每个迭代计划会上,同样会review大项目的task,评估剩余时间,只不过大项目与其他团队/部门是有时间节点的,如果剩余时间超过预期,就需要评估风险并适度增加人力。

Java中使用动态代码

O2O互联网的运营开发最大的特点就是每次运营活动规则千奇百怪,需要有许多个性化的配置,如何例A活动需要针对新用户做发红包的活动,B活动针对全部用户做发红包活动,而在B活动中针对新用户发x面额的红包,而针对老用户发y面值的红包。两个活动规则差别较大,如果每次都个性化开发,会非常浪费时间,因此如何支持规则的动态配置是个很大的挑战。

Java不是解决动态层问题的理想语言,这些动态层问题包括原型设计、脚本处理等。

公司的项目主要基于Java平台,在实践中发现主要有两种方式可以实现:

  • 统一表达式语言
  • 动态语言,如Groovy

JUEL(Java 统一表达式语言)

Java统一表达式语言(英语:Unified Expression Language,简称JUEL)是一种特殊用途的编程语言,主要在Java Web应用程序用于将表达式嵌入到web页面。Java规范制定者和Java Web领域技术专家小组制定了统一的表达式语言。JUEL最初包含在JSP 2.1规范JSR-245中,后来成为Java EE 7的一部分,改在JSR-341中定义。

主要的开源实现有:OGNLMVELSpEL ,JUEL ,Java Expression Language (JEXL) ,JEval ,Jakarta JXPath 等。这里主要介绍在实践中使用较多的MVEL、OGNL和SpEL。

OGNL(Object Graph Navigation Library)

在Struts 2 的标签库中都是使用OGNL表达式访问ApplicationContext中的对象数据,OGNL主要有三个重要因素:

  • Expression

 Expression是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析后进行的。通过Expression来告知OGNL操作到底要干些什么。因此,Expression其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量Expression,如“链式访问对象”、表达式计算、甚至还支持Lambda表达式。

  • Root对象:

    OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。

  • ApplicationContext

有个Root对象和Expression,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是ApplicationContext(上下文环境)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。

1
2
3
4
5
6
7
8
9
10
11
Foo foo = new Foo();
foo.setName("test");
Map<String, Object> context = new HashMap<String, Object>();
context.put("foo",foo);
String expression = "foo.name == 'test'";
try {
Boolean result = (Boolean) Ognl.getValue(expression,context);
System.out.println(result);
} catch (OgnlException e) {
e.printStackTrace();
}

这段代码就是判断对象foo的name属性是否为test。

OGNL的具体语法参见OGNL language guide

MVEL

MVEL最初作为Mike Brock创建的 Valhalla项目的表达式计算器(expression evaluator)。Valhalla本身是一个早期的类似 Seam 的“开箱即用”的Web 应用框架,而 Valhalla 项目现在处于休眠状态, MVEL则成为一个继续积极发展的项目。相比最初的OGNL、JEXL和JUEL等项目,而它具有远超它们的性能、功能和易用性 - 特别是集成方面。它不会尝试另一种JVM语言,而是着重解决嵌入式脚本的问题。

MVEL特别适用于受限环境 – 诸如由于内存或沙箱(sand-boxing)问题不能使用字节码生成。它不是试图重新发明Java,而是旨在提供一种Java程序员熟悉的语法,同时还加入了简短的表达式语法。

MVEL主要使用在Drools,是Drools规则引擎不可分割的一部分。

MVEL语法较为丰富,不仅包含了基本的属性表达式,布尔表达式,变量复制和方法调用,还支持函数定义,详情参见MVEL Language Guide

MVEL在执行语言时主要有解释模式(Interpreted Mode)和编译模式(Compiled Mode )两种:

  • 解释模式(Interpreted Mode)是一个无状态的,动态解释执行,不需要负载表达式就可以执行相应的脚本。
  • 编译模式(Compiled Mode)需要在缓存中产生一个完全规范化表达式之后再执行。

解释模式

1
2
3
4
5
6
7
8
9
//解释模式
Foo foo = new Foo();
foo.setName("test");
Map context = new HashMap();
String expression = "foo.name == 'test'";
VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);
context.put("foo",foo);
Boolean result = (Boolean) MVEL.eval(expression,functionFactory);
System.out.println(result);

编译模式

1
2
3
4
//编译模式
Foo foo = new Foo();foo.setName("test");Map context = new HashMap();String expression = "foo.name == 'test'";VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);context.put("foo",foo);
Serializable compileExpression = MVEL.compileExpression(expression);
Boolean result = (Boolean) MVEL.executeExpression(compileExpression, context, functionFactory);

SpEL

SpEl(Spring表达式语言)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言。 它的语法类似于传统EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。SpEL类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。

SpEL主要提供基本表达式、类相关表达式及集合相关表达式等,详细参见Spring 表达式语言 (SpEL)

类似与OGNL,SpEL具有expression(表达式),Parser(解析器),EvaluationContext(上下文)等基本概念;类似与MVEL,SpEl也提供了解释模式和编译模式两种运行模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//解释器模式
Foo foo = new Foo();
foo.setName("test");
// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);
ExpressionParser parser = new SpelExpressionParser(config);
String expressionStr = "#foo.name == 'test'";
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("foo",foo);
Expression expression = parser.parseExpression(expressionStr);
Boolean result = expression.getValue(context,Boolean.class);

//编译模式
config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, RunSpel.class.getClassLoader());
parser = new SpelExpressionParser(config);
context = new StandardEvaluationContext();
context.setVariable("foo",foo);
expression = parser.parseExpression(expressionStr);
result = expression.getValue(context,Boolean.class);

Groovy

Groovy除了Gradle 上的广泛应用之外,另一个大范围的使用应该就是结合Java使用动态代码了。Groovy的语法与Java非常相似,以至于多数的Java代码也是正确的Groovy代码。Groovy代码动态的被编译器转换成Java字节码。由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。

Groovy可以看作给Java静态世界补充动态能力的语言,同时Groovy已经实现了java不具备的语言特性:

  • 函数字面值;
  • 对集合的一等支持;
  • 对正则表达式的一等支持;
  • 对xml的一等支持;

Groovy作为基于JVM的语言,与表达式语言存在语言级的不同,因此在语法上比表达还是语言更灵活。Java在调用Groovy时,都需要将Groovy代码编译成Class文件。

Groovy 可以采用GroovyClassLoader、GroovyShell、GroovyScriptEngine和JSR223 等方式与Java语言集成。

GroovyClassLoader

GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类,也可以编译,Java代码可通过其动态加载Groovy脚本并执行。

1
2
3
4
5
6
7
class FooCompare{
boolean compare(String toCompare){
Foo foo = new Foo();
foo.name = "test";
return foo.name == toCompare;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GroovyClassLoader loader = new GroovyClassLoader();
Class groovyClass = null;
try {
String path = "FooCompare.groovy";
groovyClass = loader.parseClass(new File(path));
} catch (IOException e) {
e.printStackTrace();
}
GroovyObject groovyObject = null;
try {
groovyObject = (GroovyObject) groovyClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
result = groovyObject.invokeMethod("compare", "test");
assert result.equals(Boolean.TRUE);
System.out.print(result);

GroovyShell

GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。可以使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。

1
2
3
4
5
6
7
8
Foo foo = new Foo();
foo.setName("test");
Binding binding = new Binding();
binding.setVariable("foo",foo);
GroovyShell shell = new GroovyShell(binding);
String expression = "foo.name=='test'";
Object result = shell.evaluate(expression);
assert result.equals(Boolean.TRUE);

GroovyScriptEngine

GroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许您传入参数值,并能返回脚本的值。

FooScript.groovy

1
2
3
package blog.brucefeng.info.groovy

foo.name=="test";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Foo foo = new Foo();
foo.setName("test");
Binding binding = new Binding();
binding.setVariable("foo",foo);
String[] paths = {"/demopath/"}
GroovyScriptEngine gse = new GroovyScriptEngine(paths);
try {
result = gse.run("FooScript.groovy", binding);
} catch (ResourceException e) {
e.printStackTrace();
} catch (ScriptException e) {
e.printStackTrace();
}
assert result.equals(Boolean.TRUE);

JSR223

JSR223 是Java 6提供的一种从Java内部执行脚本编写语言的方便、标准的方式,并提供从脚本内部访问Java 资源和类的功能,可以使用其运行多种脚本语言如JavaScript和Groovy等。

1
2
3
4
5
6
7
8
9
10
11
Foo foo = new Foo();
foo.setName("test");
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine1 = factory.getEngineByName("groovy");
engine1.put("foo",foo);
try {
result = engine1.eval(expression);
} catch (javax.script.ScriptException e) {
e.printStackTrace();
}
assert result.equals(Boolean.TRUE);

使用中经常出现的问题

因此Java每次调用Groovy代码都会将Groovy编译成Class文件,因此在调用过程中会出现JVM级别的问题。如使用GroovyShell的parse方法导致perm区爆满的问题,使用GroovyClassLoader加载机制导致频繁gc问题和CodeCache用满,导致JIT禁用问题等,相关问题可以参考Groovy与Java集成常见的坑

性能对比


在这里简单对上面介绍到的OGNL、MVEL、SpEL和Groovy2.4 的性能进行大致的性能测试(简单测试):

实现方式 耗时(ms)
Java 13
OGNL 2958
MVEL 225
SpEL 1023
Groovy 99

通过这个简单测试发现,Groovy 2.4的性能已经足够的好,而MVEL的性能依然保持强劲,不过已经远远落后与Groovy,在对性能有一定要求的场景下已经不建议使用OGNL和SpEL。
不过动态代码的执行效率还是远低于Java,因此在高性能的场景下慎用。

以下是测试代码:

1
2
3
4
5
6
7
package blog.brucefeng.info.performance

class GroovyCal{
Integer cal(int x,int y,int z){
return x + y*2 - z;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package blog.brucefeng.info.performance;
public class RunPerform {

public static void main(String[] args) {
try {
int xmax = 100,ymax = 100,zmax= 10;
runJava(xmax, ymax, zmax);
runOgnl(xmax, ymax, zmax);
runMvel(xmax, ymax, zmax);
runSpel(xmax, ymax, zmax);
runGroovyClass(xmax, ymax, zmax);
} catch (Exception e) {
e.printStackTrace();
}

}

public static void runJava(int xmax, int ymax, int zmax) {
Date start = new Date();
Integer result = 0;
for (int xval = 0; xval < xmax; xval++) {
for (int yval = 0; yval < ymax; yval++) {
for (int zval = 0; zval <= zmax; zval++) {
result += xval + yval * 2 - zval;
}
}
}
Date end = new Date();
System.out.println("time is : " + (end.getTime() - start.getTime()) + ",result is " + result);

}

public static void runOgnl(int xmax, int ymax, int zmax) throws OgnlException {
String expression = "x + y*2 - z";
Map<String, Object> context = new HashMap<String, Object>();
Integer result = 0;
Date start = new Date();
for (int xval = 0; xval < xmax; xval++) {
for (int yval = 0; yval < ymax; yval++) {
for (int zval = 0; zval <= zmax; zval++) {
context.put("x", xval);
context.put("y", yval);
context.put("z", zval);
Integer cal = (Integer) Ognl.getValue(expression, context);
result += cal;
}
}
}
Date end = new Date();
System.out.println("Ognl:time is : " + (end.getTime() - start.getTime()) + ",result is " + result);
}

public static void runMvel(int xmax, int ymax, int zmax) {
Map context = new HashMap();
String expression = "x + y*2 - z";
Serializable compileExpression = MVEL.compileExpression(expression);
Integer result = 0;
Date start = new Date();
for (int xval = 0; xval < xmax; xval++) {
for (int yval = 0; yval < ymax; yval++) {
for (int zval = 0; zval <= zmax; zval++) {
context.put("x", xval);
context.put("y", yval);
context.put("z", zval);
VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);
Integer cal = (Integer) MVEL.executeExpression(compileExpression, context, functionFactory);
result += cal;
}
}
}
Date end = new Date();
System.out.println("MVEL:time is : " + (end.getTime() - start.getTime()) + ",result is " + result);

}

public static void runSpel(int xmax, int ymax, int zmax) {
SpelParserConfiguration config;
ExpressionParser parser;
config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, RunSpel.class.getClassLoader());
parser = new SpelExpressionParser(config);
StandardEvaluationContext context = new StandardEvaluationContext();
Integer result = 0;
String expressionStr = "#x + #y*2 - #z";
Date start = new Date();
for (Integer xval = 0; xval < xmax; xval++) {
for (Integer yval = 0; yval < ymax; yval++) {
for (Integer zval = 0; zval <= zmax; zval++) {
context.setVariable("x", xval);
context.setVariable("y", yval);
context.setVariable("z", zval);
Expression expression = parser.parseExpression(expressionStr);
Integer cal = expression.getValue(context, Integer.class);
result += cal;
}
}
}
Date end = new Date();
System.out.println("SpEL:time is : " + (end.getTime() - start.getTime()) + ",result is " + result);

}



public static void runGroovyClass(int xmax, int ymax, int zmax) {
GroovyClassLoader loader = new GroovyClassLoader();
Class groovyClass = null;
try {
groovyClass = loader.parseClass(new File(
"GroovyCal.groovy"));
} catch (IOException e) {
e.printStackTrace();
}
GroovyObject groovyObject = null;
try {
groovyObject = (GroovyObject) groovyClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Integer result = 0;
Date start = new Date();
for (int xval = 0; xval < xmax; xval++) {
for (int yval = 0; yval < ymax; yval++) {
for (int zval = 0; zval <= zmax; zval++) {
Object[] args = {xval,yval,zval};
Integer cal = (Integer) groovyObject.invokeMethod("cal", args);
result += cal;
}
}
}
Date end = new Date();
System.out.println("Groovy Class:time is : " + (end.getTime() - start.getTime()) + ",result is " + result);

}
}

本文的代码可以参见eldemo github

References

Groovy vs Java Performance

Groovy与Java集成常见的坑

OGNL language guide

Groovy引发的PermGen区爆满问题定位与解决

groovy脚本导致的FullGC问题

Groovy性能问题

Groovy的classloader加载机制唤起的频繁GC

Groovy深入探索——Groovy的ClassLoader体系

业务系统重构总结

之前在服务化设计模式实践 里面介绍了交易侧系统服务变迁的模式,服务的变迁更好的支持了业务的发展,伴随着业务的发展,对业务系统内部的要求也更好,需要具有更好的扩展性。随着业务的不断发展,每个服务内部的逻辑也变得越来越多,需要有更好的抽象来支持以后更多的业务类型。

1. 项目业务背景

重构的项目有订单服务,预订系统,退款系统;这三个系统都是与用户交易行为息息相关。

其中订单系统参与重构的模块为订单创建,订单状态流转,订单支付;

预订系统的重构主要为了支撑更多的预订方式,如之前已经支持的库存模式、商家接单模式和售中客服模式,伴随着重构还需要支持商家系统直连模式,而且需要能够支持以后业务发展更多的预订模式。

退款服务的复杂度主要来源于多种退款类型,如用户退款,系统退款,商家退款和客服退款等多种类型,而每种类型又有各种不同的退款规则;退款服务需要支持多种业务,如已有的KTV预订和将要扩展出的酒水点单。

在这里我们主要来讲讲预订系统重构,因为这个系统的重构几乎涵盖了订单服务和退款服务重构所使用到的技术

目标

  • 抽象预订流程,并模板化
  • 对可变化的部分支持配置化 
  • 在上线过程中支持新老流程切换 

2. 业务抽象

由图中可以看出,业务流程非常复杂,一个订单的预订过程会根据不同的情况走不同的预订渠道,如果一个预订渠道因为某种原因预订失败了,可能会继续使用另外一个预订渠道继续进行预订,也就是会发生流转。

另外,在预订成功和预订失败时,会需要做一些其他操作,例如发送短信告知用户结果等;
图中还有一点没有体现的是,在开始发起预订时,需要校验数据的正确性,校验是否复核预订规则等等校验。

根据这些条件我们做了以下抽象:

  • 首先订单从预订开始、预订中到预订成功/失败定义为预订的主流程,其中每个接单都是一个重要的业务节点,这种主流程定义为一级业务。
  • 对于不同的预订模式(如库存模式、商家接单模式、客服售中介入模式和商家系统直连等),抽象为预订渠道。预订渠道之前的转化定义为渠道流转。
  • 预订渠道会直接影响预订结果
  • 预订中、预订成功/失败 时渠道需要个性化的操作,如商家接单渠道开始时需要通知商家等,这种流程会影响一级业务,但其业务具有个性化特征,因此定义为二级业务。
  • 同时预订中、预订成功/失败后 需要进行不影响业务流程的操作,如发送短信告知用户预订结果,记录一些属性等等。这部分业务定义为三级业务。

一级业务是系统最重要的业务,业务流程标准化且会直接影响业务结果;二级业务是一级业务一个步骤,但因为预订渠道的不同而有个性化操作;三级业务是根据业务结果来执行的操作,不会再影响系统的主流程。

3. 重构

3.1 核心业务流程


预订中核心业务流程是最重要的部分,也就是图中所标注的一级业务,每一个步骤都是一个重要的业务节点,且每一个节点都会有一些复杂的逻辑。
因此在重构时,将核心业务流程的实现定义为一个模板引擎,在这个模板引擎中的每一个节点都可以是一个接口,可以任意的配置。在代码上的表现就会是这样的。

开始预订:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class KtvReserveService {

public KtvReserveResultDTO reserve(KtvReserveContext reserveContext) throws ReserveException {
// 校验
KtvValidateResult validateResult = this.ktvReserveValidateStack.validate(reserveContext);
if (validateResult == null || !validateResult.isValid()) {
return KtvReserveResultDTO.createFailedResult("validate invalid");
}
//判定预订渠道
KtvReserveChannel reserveChannel = reserveChannelJudgeService.judgeChannelType(reserveContext);
reserveDataService.store()
reserveDataService.transferReserveChannelStatus();
//开始渠道预订
ChannelResult channelResult = this.reserveChannelService.reserve(reserveContext);
return KtvReserveResultDTO
.genResult(channelResult.isSuccess(), channelResult.getDesc(), reserveFlow.getReserveId());
}

}

渠道反馈预订结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public class KtvReplyReserveService {

@Override
public ReplyReserveResult reply(KtvReplyReserveInfo replyReserveInfo) throws ReplyReserveException {
//校验
KtvValidateResult validateResult = replyReserveValidateStack.validate(replyReserveInfo);
if (validateResult == null || !validateResult.isValid()) {
logger.warn(String.format(" %s validate failed", param));
return ReplyReserveResult.createFailedResult("validate failed");
}
//更新预订状态
reserveDataService.transferReserveChannelStatus();

ReplyReserveResult result;
//判定预订结果
KtvReserveStatus toReserveStatus = this.reserveChannelJudgeService.judgeReserveResult(replyReserveInfo);
boolean reserveFailed =
toReserveStatus == null || toReserveStatus == KtvReserveStatus.ReserveFailed || toReserveStatus == KtvReserveStatus.Init;
if (reserveFailed) {
//预订失败处理
result = this.reserveFailed(replyReserveInfo);
} else if (toReserveStatus == KtvReserveStatus.ReserveSuccess) {
//预订成功处理
result = this.reserveSuccess(replyReserveInfo);
} else {
// 需要转移其他渠道预订
result = ktvReserveTransferService.transferChannel(ktvReserveContext);
}
// 渠道处理内部事务
this.replyReserveChannelService.reply(replyReserveInfo);
return result;
}
}

3.2 校验栈

在业务性很强的服务来说,在业务开始之前需要有复杂的校验,如果在这个服务中支持多种业务类型,还需要根据不同的业务类型来选择不同的校验逻辑,因此在服务中将校验栈独立出来。

校验栈的组装采用责任链模式,这样每个校验service通过组装的方式即可以灵活支持多种校验。但是对于业务主流程来说,把校验service的组装服务并不适合放在主业务流程里,因此在重构的时候将校验栈的组装逻辑放在一个单独的service中采用代理模式进行组装。

1
2
3
4
5
6
7
8
public interface KtvReserveValidateService {
/**
* 校验预订信息
* @param reserveContext
* @return
*/

KtvValidateResult validate(KtvReserveContext reserveContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class KtvReserveValidateStack implements KtvReserveValidateService {
private List<KtvReserveValidateService> validateServices;

public void setValidateServices(
List<KtvReserveValidateService> validateServices)
{

this.validateServices = validateServices;
}

@Override
public KtvValidateResult validate(KtvReserveContext reserveContext) {
if (CollectionUtils.isEmpty(validateServices)) return KtvValidateResult.validResut();
for (KtvReserveValidateService service : validateServices) {
KtvValidateResult result = service.validate(reserveContext);
if (result == null || !result.isValid()) return result;
}
return KtvValidateResult.validResut();
}
}

3.3 业务分级

在前面讲到,在重构中将代码功能分成了一级功能,二级功能和三级功能。

  • 其中一级功能的每个步骤都需要严格保证,如果发生问题就需要直接影响业务流程,例如在预订业务中,预订数据状态的更新就是一级业务,如果更新失败就需要终结业务;
  • 二级业务也是重要的业务,但是不需要二级业务不能影响最终的业务结果,但是当二级业务出错时也需要及时处理,如在更新订单状态为购买成功时发生错误,需要及时告警,或者异步化保证数据一致性;
  • 三级业务完全不影响业务流程,很多都是异步化调用外部服务,如短信通知用户、双写订单上的预订状态(老业务)等。预订服务中的三级业务都是根据预订结果而触发,因此在这里使用观察者模式实现即可。

业务服务对业务流程相对较多,而且每一步出现问题都有可能直接影响购买结果,这种与钱息息相关的业务,一单出错就会有各方来追杀,而且也极大影响用户和商家的体验。对业务分级是将不用影响最终结果的业务剥离出去,将最核心的业务重点对待,不同级别用不同的处理方式。

3.4 数据模型统一

这里的业务模型是业务流程的数据统一。例如在开始预订的业务中使用ReserveContext作为整个业务流程的数据协议,在分业务时也能采用相同的数据,即避免相同数据的重复读取也便于立减和时间。

一个业务流程的处理,其实也是一种服务的处理过程,而数据模型就是其业务的协议,好协议才能产生好实践。

3.4 机制同策略分离

机制同策略分离是Unix设计中的基本原则之一,是将将程序的引擎(程序核心域的核心算法和逻辑规格)从接口部分(接受用户命令,展示结果等)分离;因为在一个系统中策略变化相对较多,例如预订服务的三级业务中以后并不需要再同步订单上的预订状态,如果策略的变化影响到机制会使得系统很不稳定,有需求修改时会导致系统大的修改,在功能上线需要QA验证的范围也会很大;导致策略变得死板,难以适应用户需求的改变,任何策略的改变都极有可能动摇机制。
机制同策略分离的机制引用最广泛的是MVC模式。
在这里预订流程的基本模型就是我们的引擎,在引擎中规定了几个基本的业务节点,而每个业务点的实现都有各自的接口规定,如果有需求的变更只需要更改各个业务节点自身的接口实现即可。至于如何接收支付结果发起预订,以及何种情况下反馈预订接口都是与核心流程分离的。

4. 如何上线

重构最痛苦的部分是怎么把项目上线。

在这几次的重构中,主要实行了两种重构:

  • 项目内部逻辑重构,但没有新建数据表,对外的接口没有修改;
  • 修改了对外的接口,新增了数据表。

第一种模式重构,在上线时比较容易,因为基本不用考虑到新老逻辑兼容的问题,第二种模式的重构在上线时需要考虑新老接口的兼容。在这次预订服务重构过程中,修改了对外接口新增了数据记录,而且重构后的系统逻辑也与新的数据表耦合,因此在新老接口上需要做特别的兼容。这次预订服务改造主要涉及到发起预订和预订反馈,因此在兼容上需要在新老逻辑的入口上都需要做数据转换。另外在测试阶段需要模拟上线的步骤,校验上线每个阶段的新老接口兼容如何,功能是否正常。

分拆上线

一般重构的部分不宜过大,过大时需要考虑的兼容就更多,影响到的外界系统也会更多;一般重构最好的方法是分步重构,重构一部分之后验证上线,小步快跑的方式上线。

5. 总结

在这个重构的过程中我们主要有一下基本的原则:

  • 机制同策略分离
  • 协议统一化和简单化
  • 开闭原则

主要使用到一下设计模式:

  • 代理模式
  • 监听者模式
  • 责任链模式
  • 装饰者模式

Reference

UNIX编程艺术

设计模式-可复用面向对象软件的基础

设计模式wiki

服务化设计模式实践

这篇文章主要是想介绍我们团队在业务发展中业务服务的架构模式变迁,以及服务之前通信的方式变化。

服务化实现

点评在2012年左右就推荐一些共用系统服务化,到2015年公司全面推进服务化建设,业务逻辑几乎全部沉淀到后台服务,前台的web只提供简单的http 接口,而APP则只负责展示功能。

因此,在我们开始开发时就采用全部服务化的方法。点评自己开发了一套RPC框架——pigeon,这个在之前的博客RPC是什么 有过详细的介绍。

在这里主要介绍后台服务化的变迁模式。

业务背景

点评的KTV在线预订启动一年多了,一年的过程业务从小到大,再到双平台融合,业务稳定发展;这一年经历是一个大公司内部的创业构成,业务类型不断扩展,要求业务系统能够快速支持业务。

KTV在线预订是一个O2O中到店消费类,业务系统需要提供到店前决策,购买和到店消费及后期的商家结算。

业务初期(MVP阶段)

在业务初期,业务模型基本都是不清晰的,在产品提出基本的产品模型之后,需要在线上快速验证产品。

这个阶段因为要快速上线,因此我们只开发一个订单服务,这个订单服务集合了订单功能和预订功能,以及退款等功能,几乎集成了MVP阶段流程的功能。这时候各个阶段功能的数据也都是绑定在订单上的。如预订阶段的数据,退款阶段的数据等。

扩展业务

这个阶段的业务快速扩张,需要支持多种扩展的业务类型,比如预订的模式就会划分出库存模式和商家接单的模式。

  • 库存模式:是商家预先设定好当日的库存量,只要用户购买是有库存就会立即购买成功。
  • 商家接单模式:商家并没有给出确定,需要用户确定预订之后,商家才会接到通知,然后由商家决定是否接受预订,如果不接受则预订失败。

这个阶段业务开始处于快速发展中了,有了前期一定的用户和订单积累,因此这个阶段需要快速开发同时也要支持更多的业务类型。

这个阶段因为有着业务快速发展的要求,难以做到数据的独立,因此只能在服务层面做出隔离,但是因为服务都共用了一套订单数据模型,因此其他服务对订单的读取和修改也都是通过订单服务来完成的。这种模式下初步解决了业务扩展的问题,如多做预订模式,多种退款场景等;但这种方式的缺点非常明显:

  • 数据混合,导致数据模型混乱,且难以在根源上支持业务扩展;
  • 订单服务成为单点服务。

稳定发展期

业务开始进入稳定发展期,对系统的稳定性和可靠性有了更高的要求,而且这个阶段的流量也开始变高;同时也需要有新的业务类型引入。为了解决上面所提到的问题就需要通过分离数据来解决上面数据混合的问题。

这种模式情况下,相互之前的业务数据独立了,单点的服务也基本处理了,这种架构以及可以基本支持目前的业务。

但是很多服务对订单的访问基本都是查询订单数据,而这部分请求也最高的,但却并不是最重要的;对于订单业务而言,最重要的核心节点是下单和订单状态流转。因此我们在使用了上面的模式之后,渐渐将订单服务裂变出创建订单服务和订单查询服务。

未来服务的规划

目前这个阶段,有很多的逻辑需要查询订单的信息,而订单信息针对调用方来说需要订单角度的很多信息,如订单退款信息(退款时间,是否退款成功),消费状态等等;这些信息需要调用多个服务(如订单查询服务,消费服务和退款服务),而这种信息的查询需要有多个业务方来调用,如果这个功能都需要业务方查询组装的话,反而使得业务过于零散。因此在未来将会新增一些聚合服务,而原有的服务将成为基础服务;聚合服务主要是调用基础服务,并将基础服务的数据进行组装和流程处理;基础服务则定义更为基础,只处理本领域内的业务。

服务的设计模式

通过上面的模式变迁,在这个过程中,主要使用到了一下的集中服务设计模式:

  • 共享数据模式
  • 异步消息传递模式
  • 聚合期的服务模式

这些概念在之前的一篇博客我们需要什么样的微服务 中有更为详细的介绍。

References

RPC是什么

我们需要什么样的微服务

浏览器与服务器的消息通信

最近工作中遇到一个场景,商家在商家后台需要实时的获取到有没有新订单,有的话是几个;这个需求类似与日常中使用QQ或者微信时的新信息提醒一样,只要有新信息就需要提醒;商家基本在PC上使用,各式浏览器都有:如 IE系列(7.0,8.0,9.0及以上),chrome内核,firefox等;功能所属的部署在Tomcat 6.0上,如果技术需要可以部署到 Tomcat 7.0上;
我们先做做技术调研,这种浏览器与服务器实时通信的方式有哪些方式。

AJAX轮询

这是我们最自然想到的。 采用常规AJAX轮询的方式,每10s或者30s轮询一次,既可以判断出有有多少个新订单进入,且这种时间间隔对于消息提醒也是可以接受的。这种技术方式实现起来非常简单,目前的机器都是可以机器的,前端浏览器也都支持。
但是这种方式会有非常严重的问题,就是需要不断的向服务器发送消息询问,如果有1w个商家打开了浏览器,采用10s轮询的方式,则服务器则会承担1000 的QPS,这1w个商家可能只有10个有订单通知;这种方式会对服务器造成极大的性能浪费。
还有一个类似的轮询是使用JSONP跨域请求的方式轮询,在实现起来有差别,但基本原理都是相同的,都是客户端不断的向服务器发起请求。

优点
实现简单。
缺点
这是通过模拟服务器发起的通信,不是实时通信,不顾及应用的状态改变而盲目检查更新,导致服务器资源的浪费,且会加重网络负载,拖累服务器。

Comet

Comet是一种用于Web的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式:

长轮询(long polling)

长轮询 (long polling) 是在打开一条连接以后保持,等待服务器推送来数据再关闭,可以采用HTTP长轮询和XHR长轮询两种方式。

HTTP 和JSONP方式的长轮询

把 script 标签附加到页面上以让脚本执行。服务器会挂起连接直到有事件发生,接着把脚本内容发送回浏览器,然后重新打开另一个 script 标签来获取下一个事件,从而实现长轮询的模型。

XHR长轮询

这种方式是使用比较多的长轮询模式。
客户端打开一个到服务器端的 AJAX 请求然后等待响应;服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接;如此循环。
现在浏览器已经支持CROS的跨域方式请求,因此HTTP和JSONP的长轮询方式是慢慢被淘汰的一种技术,建议采用XHR长轮询。

长轮询优缺点

优点
客户端很容易实现良好的错误处理系统和超时管理,实现成本与Ajax轮询的方式类似。
缺点
需要服务器端有特殊的功能来临时挂起连接。
当客户端发起的连接较多时,服务器端会长期保持多个连接,具有一定的风险。

iframe

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。

优点:
这种方式每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。
缺点
IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。
Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth’s of Google’s amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。
我们常用的网页版的gtalk就是这种实现方式,Google的开发人员使使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题。

Comet实现框架

CometD

CometD 框架是基于 HTTP 的事件驱动通信解决方案,使用了Bayeux通信协议,提供了一个 Java 服务器部件和一个 Java 客户端部件,还有一个基于 jQuery 和 Dojo 的 JavaScript 客户端库。
>
Bayeux 通信协议主要是基于 HTTP,提供了客户端与服务器之间的响应性双向异步通信。Bayeux 协议基于通道进行通信,通过该通道从客户端到服务器、从服务器到客户端或从客户端到客户端(但是是通过服务器)路由和发送消息。Bayeux 是一种 “发布- 订阅” 协议。

CometD 与三个传输协议绑定在一起:JSON、JSONP 和 WebSocket。他们都依赖于 Jetty Continuations 和 Jetty WebSocket API。在默认情况下,可以在 Jetty 6、Jetty 7、和 Jetty 8 中以及其他所有支持 Servlet 3.0 Specification 的服务中使用 CometD。
服务器和内部构件

Atmosphere框架

Atmosphere提供了一个通用 API,以便使用许多 Web 服务器(包括 Tomcat、Jetty、GlassFish、Weblogic、Grizzly、JBossWeb、JBoss 和 Resin)的 Comet 和 WebSocket 特性。它支持任何支持 Servlet 3.0 Specification 的 Web 服务器。

Atmosphere 提供了一个 jQuery 客户端库,该库可以使连接设置变得更容易,它能够自动检测可以使用的最佳传输协议(WebSockets 或 CometD)。Atmosphere 的 jQuery 插件的用法与 HTML5 WebSockets API 相似。

Pushlet

Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。
Pushlet 最后更新于2010年2月5号,之后至今没有再更新。

Cometd 和Atmosphere框架参见示例代码 (https://github.com/brucefengnju/cometdatoms)。

Comet实现要点

不要在同一客户端同时使用超过两个的 HTTP 长连接
HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞,在IE浏览器中严格遵守了这种规定。

服务器端的性能和可扩展性
一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。

在客户和服务器之间保持“心跳”信息
在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道双方都在正常运行。
服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。

[websocket] (https://zh.wikipedia.org/wiki/WebSocket)

WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通讯协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

浏览器支持

浏览器 版本支持
Chrome 4+
Firefox 4+
IE 10+
Opera 10+
Safari 5+

详情查看 Browser compatibility

实现

WebSocket的实现已经有很多种版本,详细可以查看DEMO

总结

总结下来长轮询不是一个很好的方案,而且对于服务器而言是有风险的;另外支持WebSocket协议的浏览器都比较新,特比是IE需要10以上的版本;而我们的业务是面向于商家端,商家的浏览器版本相对较低,很多对WebSocket都不支持;相对而言Comet的方式比较适合,也有相应的实现框架,实现成本最低;因此最后我们还是决定使用Comet的方式来实现,后面上线运行一段时间之后再来给大家介绍。

References

comet wiki

Comet:基于 HTTP 长连接的“服务器推”技术

反向 Ajax,第 1 部分: Comet 简介
What else is burried down in the depth’s of Google’s amazing JavaScript?
WebSocket 是什么原理?为什么可以实现持久连接
Ajax、Comet、HTML 5 Web Sockets技术比较分析
WebSocket wiki

使用 HTML5 WebSocket 构建实时 Web 应用

The WebSocket Protocol
How can I use HTTP/2 server push?
HTTP/2 Push with experimental Servlet API
HTTP/2 Interoperability and HTTP/2 Push
/google-talk-client-in-javascript

date: 2016-02-16 09:54:53
author: sinopex
email: wsj6563@gmail.com
site:
ip: 116.231.180.8

楼主好励志!!!


前端跨域访问

在互联网应用中:

  • 一个页面需要请求多个域名下的web服务端接口
  • 同时一个web服务接口可能会被很多不同域名下的页面请求。

一个web应用如果支持为了支持以上模式而申请多个域名是不合算的,因为域名申请和管理所占用的资源比较大,因此服务端支持跨域就成了一个更合理的解决方案。
解决跨域的方式主要有两种:

1. JSONP

关于JSONP的基本概念就不多介绍了,现在在网上已经有很多解释。JSONP模式下前端Client可以跨域请求JSON文件,进而实现前端跨域请求其他服务器资源的目的。
目前在一些流行的JavaScript库中对JSONP和Ajax的支持方式在表现形式上非常相近,在代码中写法几乎都是相同的,但是JSONP与Ajax是完全不同的原理。JSONP是以请求文件数据的方式向服务器发出请求,而Ajax是使用XMLHttpRequest向服务器异步发出请求。

  • 优点
    JSONP并不需要浏览器特殊支持,可以说所有的浏览器都是支持JSONP请求的。而且目前各流行JavaScript类库对JSONP的支持已经很全面,开发中也比较方便。

  • 缺点
    但JSONP的请求只能是GET请求,因为在请求URL有长度限制,一般情况下只要不超过2000字符都是可以的(What is the maximum length of a URL?),主流浏览器所支持的长度也越来越放宽,基本是可以满足条件的。
    JSONP的web服务端接口因为无法限制接收指定域名的请求,因此在实际应用中需要在安全性方面进行更多限制,以避免接口数据泄漏。

2. CORS(Cross-origin resource sharing)

主要是通过定义浏览器与服务器之间共享内容的方式来实现跨域。
CORS通过新增一系列 HTTP 头(Access-Control-Allow-Origin,Access-Control-Expose-Headers,Access-Control-Max-Age,Access-Control-Allow-Credentials,Access-Control-Allow-Methods,Access-Control-Allow-Headers等),让服务器能声明那些来源可以通过浏览器访问该服务器上的资源。另外,对那些会对服务器数据造成破坏性影响的 HTTP 请求方法(特别是 GET 以外的 HTTP 方法,或者搭配某些MIME类型的POST请求),标准强烈要求浏览器必须先以 OPTIONS 请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 Cookies 和 HTTP 认证相关数据)。

2.1 运行模式

如果只使用简单请求向服务器发出请求,则浏览器就不需要向服务器发送预请求,服务器端只需要在response中增加Access-Control-Allow-Origin就可以了,开发非常简单,在代码上和Ajax请求几乎没有区别。

简单请求:只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。不会使用自定义请求头(类似于 X-Modified 这种)。
但请求以如果 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求或者使用自定义请求头(比如添加诸如 X-PINGOTHER)时,浏览器就需要向服务器发送预请求,以确定服务器是否支持后续请求,如果支持,浏览器则继续发送后续Ajax请求。

2.2 JQuery支持CORS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$.ajax({
//PUT 和DELETE 需要发送预请求
type: 'HTTP METHOD',//如GET ,POST
url: 'cross-domain-url',
contentType: ,

xhrFields: {
withCredentials: false
},

headers: {
},

success: function() {
},

error: function() {
}
});

详细的使用参见HTTP访问控制(CORS)

2.3 与JSONP相比

CORS的开发更为简单,对安全性的控制更为灵活,且目前所有的现代浏览器都已经支持了CORS模式,CORS支持所有的HTTP Method类型,在Restful请求中可以实现跨域。

3. 跨域访问在点评的应用

在点评,很多功能采用动静分离的方式,在项目部署上也采用前端静态文件资源与后端web服务独立域名的方式进行;这种模式下,后端的HTTP 接口对于前端而言是一个服务接口;例如一个查询用户是否登录的接口,可能会被多个项目使用,因此就要求服务端接口支持跨域请求。
目前在点评中主要使用JSONP的方式来实现跨域请求;因为CORS算是一个新技术,因此还没有大规模使用,只会在一些不重要的功能中使用过。

References

JSONP
What is the maximum length of a URL?
CORS(Cross-origin resource sharing)
Cross-Origin Resource Sharing
HTTP访问控制(CORS)
JQuery CORS support IE’s XDomainRequest object plugin

转载—高可用性系统在大众点评的实践与经验

这是一篇完全转载的文章(原文地址),里面的很多经验都是经历过多血泪史。


高可用性系统在大众点评的实践与经验

2016-01-21陈一方 from 井底之蛙

本文主要以点评的交易系统的演进为主来描述如何做到高可用,并结合自己的经验作些分享。高可用性只是一个结果,要更多的关注迭代过程,关注业务发展。

主要从下面几个方面来分享

  • 可用性的理解

    1. 理解目标
    2. 目标分解
  • 频率要低

    1. 高可用性的设计
    2. 易运营&测试
    3. 关注发布
  • 时间要快

    1.持续关注运行时

    2.充分利用故障时

  • 几点经验
    1.珍惜每次大流量机会

    3.可用性不只是技术问题

    4.可用性最大的敌人

一、可用性的理解

1.理解目标

业界高可用的目标是几个9,对于每一个系统的要求是不一样的,对研发人员来说,要对设计或者开发的系统知道其用户规模及使用场景,知道可用性的目标。
比如5个9的目标能分解:全年故障5分钟。

2.拆解目标

几个9的目标比较抽象,需要对目标进行合理的分解,可以分解成如下两个子目标

2.1 频率要低:减少出故障的次数。

不出问题,一定是高可用的,但这是不可能的。系统越大、越复杂,只能尽量避免问题,通过系统设计,流程机制来减少这种问题概率。但如果经常出问题,后面的恢复再快也是没有用的。

2.2 时间要快:故障恢复时间要快

故障出现时,不是解决或者定位到具体问题,而是快速恢复是第一要务的,防止次生灾害,问题扩大。这里就要求要站在业务角度思考,而不仅是技术角度思考。

二、频率要低:减少出故障的次数

1.高可用性的设计:根据业务变化不断进行迭代。

以点评交易系统的演进过程举例

1)幼儿时期:2012

使命:满足业务要求,快速上线。

2011年要快速的把团购产品推向市场,团队都是临时从各个团队抽取的人才,大部分对.net更熟悉,所以使用.net进行了第一代的团购系统设计,满足业务要求是第一的,还没有机会遇到可用性等质量问题。考虑比较简单,要挂都挂了,量也比较小,出现问题,重启,扩容,回滚就解决问题了。

系统长成下图这样:

2)少年时期:垂直拆分(2012-2013)

使命:研发效率&故障隔离
当2012年在团单量从千到万量级变化,用户每日的下单量也到了万级时候,需要考虑的是迭代速度,研发效率。所以要做小而美的团队。另外一方面也需要将各个业务相互隔离,比如商品首页的展示,商品详情页的展示,订单、支付流程的稳定性要求不一样。前面可以缓存,可以做静态化来保证可用性,提供一些柔性体验。后面支付系统做异地容灾,比如我们除了南汇机房支付系统,在宝山机房也部署了,只是后来发现这个系统演进太快,没有工具和机制保证双机房更新,所以后来也不好使用了。

系统演进成下图这样:这个就是服务垂直化了,但是数据没有完整隔离开,互相摸。

3)青年时期:服务做小,不共享数据(2014-2015 )

使命:支撑业务快速发展,提供高效、高可用的技术能力
从2013年开始,deal-service (商品系统)偶尔会因为某一次大流量(大促,常规活动)会挂,每几个月总有那么一次。基本上可用性就在3个9徘徊,这里订单和支付系统很稳定的,因为流量在商品详情页到订单有一个转化率,流量大了详情页就挂了,订单也就没有流量了,后来做了详情的静态化做得比较好了,能减少恢复的速度,能降级,但是deal-service的各个系统依赖太深了,还是不能保证整体端到端的可用性。

所以2014年deal-service就做了很大的重构,大系统做小,把商品详情系统拆成了无数小服务,比如库存服务,价格服务,基础数据服务等等。这下商品详情页的问题解决了,所以从2014年低订单系统的压力就来了,前面好了,后面压力就来了。2014年10月起,订单系统,支付系统也启动了全面微服务化,经过大约1年的实践,订单系统、促销系统、支付系统这3个领域后面的服务总和都快上百个了,后面对应的数据库20多个,这样能支撑到每日订单量百万级。

业务的增长在应用服务层面是可以扩容的,但是最大的单点,数据库是集中式的,这个阶段我们主要是把应用的数据访问在读写上分离,数据库提供更多的从库解决读的问题,但是写入仍然式最大的瓶颈(mysql的读可以扩展,写入QPS 也就小2万)。

系统演变成下图这样:这个架构大约能支撑QPS 3000左右的订单量。

4)成年时期:水平拆分(2015-现在)使命:

系统要能支撑大规模的促销活动,订单系统能支撑每秒几万的QPS,每日上千万的订单量。

2015年的917吃货节,流量最高峰,如果我们仍然是前面的技术架构,必然会挂掉,所以在917这个大促的前几个月,我们就在订单系统进行了架构升级,水平拆分,核心就是解决数据单点,把订单表拆分成了1024张表,分布在32个数据库,每个库32张表。这样能支撑到我们能看见到未来了。

虽然数据层的问题解决了,但是我们还是有些单点,我们用的MQ,网络,机房等。举几个曾经我过去遇到的不容易碰到的可用性问题:

1)服务的网卡有一个网卡坏了,没有被监测到,后来发现另一个网卡也坏了,这样服务就挂了。

2)我们使用 cache的时候发现可用性在高峰期非常低,后来发现这个cache服务器跟公司监控系统cat服务器在一个机柜,高峰期的流量被cat跑了一大半,给业务的网络流量就非常少,就影响了业务。

3)917大促的时候我们对MQ这个依赖的通道能力评估出现了偏差,也没有备份方案,所以造成了一小部分的延迟。

这个时期系统演进下图这样:

5)未来:思路仍然是大系统做小,基础通道做大,流量分块。

大系统做小,就是把复杂系统拆成单一职责系统,并从单机、主备、集群、异地等架构方向扩展。

基础通道做大就是把基础通信框架,带宽等高速路做大。

流量分块就是把用户流量按照某种模型拆分,让他们聚合在某一个服务集群完成,闭环解决。

系统可能会演变下下图这样:

上面点评交易系统的发展几个阶段,只以业务系统的演进为例,除了这些还有CDN,DNS、网络、机房等各个时期遇到的不同的可用性问题,我们遇到过的问题,比如:联通的网络挂了,需要切换到电信;比如数据库的电源被人踢掉了。

2.易运营

高可用性的系统一定是可运营的,听到运营,大家更多的想到的是产品运营,其实技术的运营指的是线上的质量,流程能否运营,比如,整个系统上线后,是否方便切换流量,是否方便开关,是否方便扩展。这里有几个基本要求:
1)可限流
线上的流量永远有想不到的情况,在这种情况下,系统的稳定吞吐能力就非常重要了,高并发的系统一般采取的策略是快速失败机制,比如系统QPS能支撑5000,但是1万的流量过来,我能保证持续的5000,其他5000我快速失败,这样很快1万的流量就被消化掉了。比如917的支付系统就是采取了流量限制,如果超过某一个流量峰值,我们就自动返回请稍后再试等。
2)无状态
应用系统要完全无状态,运维才能随便扩容,分配流量。
3)降级能力
降级能力是跟产品一起来看的,需要看降级后,对用户的体验的影响,简单的比如,提示语是什么,比如我们支付渠道,如果支付宝渠道挂了,我们挂了50% ,我们支付宝的渠道是旁会自动出现一个提示,这个渠道可能不稳定,但是可以点击;当支付宝渠道挂了100% ,我们的按钮是灰色的,不能点击。也会有提示,比如换其他支付渠道(刚刚微信支付还挂了,就又起作用了)。另一个案例,我们在啊917大促的时候对某些依赖方,比如诚信的校验,这种如果判断比较耗资源,又可控的情况下,可以通过开关直接关闭或者启用。

3.可测试

无论架构多么完美,验证这一步必不可少,系统的可测试行就非常重要。
测试的目的要先预估流量的大小,比如某次大促,要跟产品、运营讨论流量的来源,活动的力度,每一张页面的,每一个按钮的位置,进行较准确的预估。
再测试集群的能力,有很多同学在实施的时候总喜欢测试单台,然后水平放大,给一个结论,但这不是很准确,要分析所有的流量是否在系统间流转时候的比例。尤其对流量模型的测试(要注意高峰流量模型跟平常流量模型可能不一致的)系统架构的容量测试,比如我们某一次大促的测试方法
从上到下评估流量,从下至上评估能力:发现一次订单提交 有20次数据库访问,读写比例高峰期是1:1,然后就跟进数据库的能力倒推系统应该放入的流量,然后做好前端的异步下单,让整个流量平缓的下放到数据库。

4. 降低发布风险

4.1 严格的发布流程

目前点评的发布都是开发自己负责的,通过平台自己完成的,上线的流程,发布的常规流程模版:

4.2 灰度机制

1)服务器发布是分批,按照10%,30%,50%,100%的发布,开发通过观察监控系统的曲线,及系统的日志确定业务是否正常。
2)线上的流量灰度机制,重要功能上线能有按照某种流量灰度上线能力。
3)可回滚是标配,最好有最坏情况的预案。

三、时间要快:故障恢复时间要快

如果目标就要保证全年不出故障或者出了故障在5分钟之内能解决,要对5分钟进行充分的使用,对5分钟的拆解:1分钟发现故障,3分钟定位故障出现在哪个服务,再加上后面的恢复时间。就是整个时间的分解,目前我们系统大致能做到前面2步,离整体5个9的目标还有差距,因为恢复的速度跟架构的设计,信息在开发、运维、DBA之间的沟通速度及工具能力,及处理问题人员的本身能力有关。
生命值:

1.持续关注线上运行情况
1)熟悉并感知系统变化,要快就要熟,孰能生巧,所以要关注线上运营情况。
对应用所在的网络、服务器性能、存储、数据库等系统指标了解。
能监控应用的执行状态、对应用的自己QPS、响应时间、可用性指标,并对依赖的上下游的流量情况同样熟悉。
2)保证系统稳定吞吐 :系统如果能做好流量控制,容错,保证一个稳定的吞吐,能保证大部分场景的可用,也能很快的消化高峰流量,避免出现故障,产生流量的多次高峰。
2.故障时
2.1.快速的发现机制
1)告警的移动化 :系统可用性的告警应该全部用微信、短信这种能保证找到人的通信机制。
1)告警的实时化:目前我们只能做到1分钟左右告警。
3)监控的可视化:我们的系统目前的要求是1分钟发现故障,3分钟定位故障:这就需要做好监控的可视化,在所有关键service里面的方法层面打点,然后做成监控曲线,不然3分钟定位到具体是那个地方出问题,比较困难。点评的监控系统cat能很好的提供这些指标变化,我们系统再这些基础上也做了一些更实时的能力,比如订单系统的我们的QPS 就是开发的秒级的监控曲线。

2.2.有效的恢复机制

比如运维的四板斧:回滚、重启、扩容、下服务器。在系统不是很复杂,流量不是很高的情况下,这能解决问题,当大流量的时候这个就很难解决了,所以更多的从流量控制、降级体验方面下功夫

四、几点经验

1.珍惜每次真实高峰流量,建立高峰期流量模型。
2.珍惜每次线上故障复盘,上一层楼看问题,下一楼解决问题。
3.可用性不只是技术问题
系统初期是:以开发为主;
系统中期是:开发+DBA+运维为主;
系统后期是:技术+产品+运维+DBA ;
4.单点和发布是可用性最大的敌人

2016新年计划

2015年写了一次新年计划,今天继续吧。
先来看看2015年做的怎么样了。

2015年完成了什么

2015新年计划 写了5件事情。

  • 一个月读一本书

    这个已经完成了,只是没有一本本记录过,今年做下记录。
    完成度:★★★★★

  • 出去旅游一次

    今年旅游了两次,一次是五一去半山温泉做了下自驾游,还有就是最近台湾游
    完成度:★★★★★

  • 学习一门新的编程语言
    之前一直学习Clojure,可惜学了好久的语法,到最后也不想继续再坚持写项目了。现在开始学Golang了,不过进展也不大。这个任务完成的不好
    完成度:★★☆☆☆
  • 研究一个开源项目
    今年学习变成语言的时候断断续续的研究了许多项目,但是一直没能深入下去一个,更没有参与到其中了。完成的也不够好。
    完成度:★★☆☆☆
  • 工作上再升级
    今天在工作上成长了许多,包括自己的设计能力,业务项目的架构能力和技术管理的能力。至于级别能不能升已经不在意了,更重要的是自己的成长。
    完成度:★★★★★

2016年要做什么

1. 读书计划

继续每月一本书。
几年要少看点小说,多看些社会历史、投资理财的书籍。

2. 学会英语

每年学会一个语言,今年学习英语。

英语一直没有学会,今年达到英语熟练读写说的程度。

3. 个人成长

变得平和。

30岁了,对人对事需要更成熟些,遇到事情不能再像以前那样急躁了,变得平和些,遇事稳重些。

4. 工作上转变为管理者

一直拒绝变成管理者,在工作上还是更想做一个技术人员;单现实工作却越来越要求自己转变为一个技术管理者,以前的想法导致自己难以胜任现在的工作,使得大家都别扭。
新的一年,自己彻底转变成技术管理者,在打磨技术的同时让自己的管理能力更上一层楼。

5. 凡事多思考

在工作中、生活中遇到一些问题和新事物都认真思考,考虑清楚再来做决定。

怎么才叫思考清楚呢?就是当自己想要说服别人的时候,别人提过的问题自己都能回答,至少自己都思考到了。

今年也是五条计划,再接再厉。

date: 2015-12-14 14:23:11
author: 在线工具
email: wzwahl36@qq.com
site: http://www.atool.org/
ip: 123.58.191.68

可以加一些东西,比如:对于有已经有的没有动静分离的网站,如何快速动静分离~