架构师进阶之《做踏实的架构》
做踏实的架构
1 引言
架构
重要性
讨论非常多,各个层面都有
感觉很重要,但是无从下手。
我这样做是否能达成目标,没有很直观的关联。
关键点
我做出来的东西和我想满足的目标,是否有一个明显的关联
什么是架构?
定义非常多:
系统有很多组成部分,有很多关联,共同协作达成一定的目标。非常形式化的描述,很多系统都可以这样描述。
哲学观:一切都是对象,一切都是进程。。。。
系统里面最重要的一些东西。也是一个含糊的东西。。。
系统里面难以改变的东西。我们希望是推迟决策的。
如何做架构?
瀑布:很长的阶段专门做架构
迭代模型:以迭代的方式,并不是专门的时间专门做架构
结论
无论怎么定义架构,无论怎么去做架构,有一点是一定的:
架构一定是以设计构造的方式体现出来,并且对应你的目标。
另外:
1.你一定要对你的设计构造非常清楚
2.一定要和你的目标有关联的
2 主要设计构造
模块化
模块化是一个设计构造。我希望我设计的东西是模块化的。
是一种最基础的设计构造。
灵活性,扩展性,可重用性等好处。
但是这个模块化并不仅仅是我们平时认为的模块化的,是计算层面的。
数据结构和算法
我怎么选择数据结构和算法来满足我的业务特征。
并发和并行
并发和并行也是一种设计构造,它解决什么问题?
响应性非常好;也可能是我的计算资源;也可能是为了可用性
通信模型
共享内存,异步的。。
和我的业务特征是什么关联关系
数据存储和处理
关系型还是非关系型。
我的事务级别是什么?
这些完全是针对我锁面对的业务特点的。
不确定性
这是软件开发里面困难的根源!
一定会面对我的不确定性,我有哪些设计构造来面对我的不确定性。
Crash Oriented:
1.这样的结果往往系统的可靠性更高。
2.一种设计思想
3.也是需要设计构造
微服务
是一种架构风格,也可以当成设计构造。
系统很大很多部件,可以通过异构的方式构建我的系统。
3 第一原则:understandable
可理解性,这是做设计决策的第一原则。
并不是“模式”和“原则”的满足
当然,并不是说模式和原则不重要,但是被放到过高的位置上了,远远超过了它能起到的作用。并不是我懂得模式和原则就懂得了设计。也并不是说我的设计满足了各种各样的原则就是好的设计。
关键点
Resoning
推理。就是编出来,自动可以验证是否是正确的。这个虽然是一个热点,但是实际上遇到了很多困难。
不是通过测试来判断,而是通过证明。
正确性
首先你得理解了才能知道是否是正确的。不理解就没法判断。很多时候干啥我也不知道,但是测试通过了。
状态
过于强调静态层面的东西,结构上的东西。其实对于上规模的系统,动态实体的更重要。实体处于什么状态更重要。
比如说在任意的一个时间点,让系统“暂停”下来,能不能清楚的知道系统中有哪些实体和状态。
控制
控制流程如何运转。
结构
有些系统是以结构为中心,有些是以状态为中心。大部分系统是混合的。
性能
不一定把它优化的很快很快,你要知道,哪些地方是性能瓶颈/热点,哪些地方不是。
高层面的要用架构的层面上去解决,更细粒度的一定要通过 profile 来做。
组织方式
代码组织
发布版本等
例子: Fib
例子:search
list
binary tree
hash
现在编程库比较发达,可以随便选。但是并不是随意的去选择,我们一定要理解。
4 Modularity
Function Decomposition
功能分解。
最开始是流水线形式,一个程序就是完成一个任务。
一开始就一个函数,后来发现,这样写出来的程序不好理解。因此就有了功能分解。然后共享数据。
有时候就是以过程为中心,数据相对稳定,扩展点都在处理上。那这种方法就非常非常合适。
ADT & OO
数据抽象。
我们发现数据总变,没变一次很多过程都要改。
把数据,数据的使用,还有我的表达,分开。提供一个使用接口。
如果我的领域里,数据更重要,处理数据的过程不那么重要,就可以用这种方式。
大部分情况是混合的。
例子
stack:push() pop()
这要满足这种约束都是堆栈。
C 语言也可以实现
OO
OO 更高级,不仅仅是分开了,而且在运行时能判断使用哪一个。
问题
对象的协作很复杂的情况,需要新做一层就协调对象的交互。
职责分派
协作
判断标准就是哪种方式好理解。这是我做面向对象设计的参考标准。
关于重构
OO 的作用是消灭圈复杂度。定义一个接口,if else 就没有了,问题在于改完之后问题有没有变得好理解。
OO 在实现层面是就是个动态分派表,所以我们在消除 if else 的时候是在用面向对象的实现而不是面向对象的思想。
其实完全不用这样也可以达到这样的目的。
High order calculation
强调一种关系。
Relation
在 Function Decomposition 里面过程重要,在 OO 里面数据重要,但其实在另外的领域里面还有一种东西重要:
Relation
函数
数学意义上的函数,其实就是关系的一种。
从一个对象到另一个对象的映射。
first class
函数和其他基本类型的地位是一样的。可以起个名字,作为参数,返回值。
Lazy Evaluation
c/c++都是严格的语言,任何语句拿到之后一定是先求值的。问题是你如何在 C/C++语言里表达一个无限的 LIST,能不能定义一个变量表示所有整数
Algebrac
真正的数学里面我可以写一个表达式:
1+2+3*4+(a+b)。。。。
我交换位置计算一定不影响结果。
我的代码里看起来一样,但是就是不能替换,不能交换。因为是上下文相关的。
代数性质
是我们编程中追求的一种性质。
Language Oreinted
自己定义一种语言结构,面对我的问题领域。底层在实现编译器/解释器来支撑。
5 FizzBuzzWhizz
体验真正的模块化。计算层面的模块化。
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
typedef int (*condition)(int num);
typedef char (action)(int num);
enum RuleType {RuleTypeAtom,RuleTypeAdd,RuleTypeOr};
struct AtomRule
{
condition _cond;
action _act;
};
struct Rule;
struct AddRule
{
struct Rule* rules[2];
};
struct OrRule
{
struct Rule* rules[2];
};
struct Rule
{
enum RuleType _type;
union {
struct AtomRule _atom;
struct AddRule _add;
struct OrRule _or;
}_rule;
};
struct RuleResult
{
int _satisfied;
char _output[100];
};
struct RuleResult apply_atom(int num, struct AtomRule atom)
{
struct RuleResult result = {0, {0}};
if (atom._cond(num))
{
result._satisfied = 1;
strcpy(result.output, atom.act(num));
}
else
{
result._satisfied = 0;
}
return result;
}
struct RuleResult apply(int num, struct Rule* rule);
struct RuleResult apply_add(int num, struct AddRule add)
{
struct RuleResult result = apply(num, add.rules[0]);
if (result._satisfied)
{
struct RuleResult result2 = apply(num, add.rules[1]);
if (result2._satisfied)
{
strcat(result.output, result2.output);
}
return result;
}
return apply(num, add.rules[1]);
}
struct RuleResult apply_or(int num, struct OrRule or)
{
struct RuleResult result = apply(num, or.rules[0]);
if (result._satisfied)
{
return result;
}
return apply(num, or.rules[1]);
}
struct RuleResult apply(int num, struct Rule* rule)
{
struct RuleResult result = {0, {0}};
switch(rule->_type)
{
case RuleTypeAtom:
{
result = applyatom(num, rule->rule._atom);
break;
}
case RuleTypeAdd:
{
result = applyadd(num, rule->rule._add);
break;
}
case RuleTypeOr:
{
result = applyor(num, rule->rule._or);
break;
}
}
return result;
}
int times_3(int num)
{
return num%3 == 0;
}
int times_5(int num)
{
return num%5 == 0;
}
int times_7(int num)
{
return num%7 == 0;
}
char* to_fizz(int num)
{
return "fizz";
}
char* to_buzz(int num)
{
return "buzz";
}
char* to_whizz(int num)
{
return "whizz";
}
struct Rule* atom(condition cond, action act)
{
struct Rule rule = (struct Rule)malloc(sizeof(struct Rule));
rule->_type = RuleTypeAtom;
rule->rule.atom._cond = cond;
rule->rule.atom._act = act;
return rule;
}
struct Rule or(struct Rule r1, struct Rule* r2)
{
struct Rule rule = (struct Rule)malloc(sizeof(struct Rule));
rule->_type = RuleTypeOr;
rule->rule.or.rules[0] = r1;
rule->rule.or.rules[1] = r2;
return rule;
}
struct Rule or3(struct Rule r1, struct Rule r2, struct Rule r3)
{
return or(or(r1,r2),r3);
}
struct Rule or4(struct Rule r1, struct Rule r2, struct Rule r3, struct Rule* r4)
{
return or(or3(r1,r2,r3),r4);
}
struct Rule add(struct Rule r1, struct Rule* r2)
{
struct Rule rule = (struct Rule)malloc(sizeof(struct Rule));
rule->_type = RuleTypeAdd;
rule->rule.add.rules[0] = r1;
rule->rule.add.rules[1] = r2;
return rule;
}
struct Rule add3(struct Rule r1, struct Rule r2, struct Rule r3)
{
return add(add(r1,r2),r3);
}
int contain_3(int num)
{
return (num%10 == 3) || ((num/10)%10 == 3);
}
int always_true(int num)
{
return 1;
}
char* to_num(int num)
{
static char num_string[6];
sprintf(num_string, "%d", num);
return num_string;
}
int main(int argc, char const *argv[])
{
struct Rule* rule13 = atom(times3, to_fizz);
struct Rule* rule15 = atom(times5, to_buzz);
struct Rule* rule17 = atom(times7, to_whizz);
struct Rule* rule1 = or3(rule13,rule15,rule1_7);
struct Rule* rule2 = add3(rule13,rule15,rule1_7);
struct Rule* rule3 = atom(contain3,tofizz);
struct Rule* ruledefault = atom(alwaystrue,to_num);
struct Rule* rule = or4(rule3,rule2,rule1,rule_default);
int i = 0;
for (i = 1; i <= 100; ++i)
{
struct RuleResult result = apply(i, rule);
printf("%s\n",result._output);
}
return 0;
}
6 Functional Reactive Programming
做 UI 设计,后端的数据处理很有用。
reactive 很老的设计方式,因为目前的复杂性提高了,才又被提起。
例子:UI 控件
事件什么时候来,是不知道的。
通常的做法,有一个按钮处理,然后加 callback 回调。回调里有很多逻辑。逻辑全部分散在各个 callback 程序里了。
7 再谈面向对象
面向对象语言把对象间的通信方式固定死了,就是 fanction all,这就导致了,如果想把对象的语义扩大到分布式系统里面,会遇到很多很多问题。
这也催生了像 colba 技术, Actor 这样的模式的出现。
8 数据结构和算法
决定了能不能用,没有可争辩的。作为程序员来说,这个东西最重要,第一个应该学这个,设计应该放到后面。
关键点
复杂度
Sort,Search
List,Stack,Queue,Hash,Tree,Graph
Advanced:
Dynamic programming
Amortization analysis
string matching
RB-tree, Skip-list
Approximation
建立直觉关联
实例:电话计费系统
匹配最长电话号码。
实例:找中位数
基本算法
一万亿个数字
变成一个系统性问题了。
需要占多少内存
怎么解决分布式的问题
9 并发和并行
什么是并发?什么是并行?
并发
历史:最开始都是批处理。做完第一个做第二个。。。。并且是确定性的,程序的执行结果是固定的,没有交互。
但是有些系统依赖外部的输入,这时候就不是确定性,里面有很多不确定性的东西在里面。也就是输入是不确定的,结果是不确定的。
事件驱动。比如来个鼠标点击,就干一件事情。如果用批处理的方式,就是不断轮询检测。但是这样必须干完一件事情再干另一件时间。
并发---事件之间没有关联。可以一起做。用不同的并发体,处理不同的事件。
好处
更好的模块化
在多核环境下,可以提升响应性。
问题
程序是一个整体,不同并发体之间需要关联。比如访问同一个数据区。
必须进行同步。
应用不当的话,带来的坏处更大。
无锁数据结构
死锁:系统没有任何进展。
lock free;保证系统当中至少有一个线程是往前走的。并不是没有锁,提供 CAS 等底层机制。
Software Transation Memory
GC 对于内存的解放,STM 对于并发的解放。
并行
并发是一种实现机制。并行是一种模型,和实现没有关系。
例子: Software Transation Memory
假设我有一块内存里有两个变量。有两个独立的并发用户,并发访问修改。
如何不用锁?
通过版本号。
问题:
一个很快,一个很慢,可能永远写不进去。
解决:
策略,让很快的等一会。
MVCC
也是这种思想。
怎么利用多核
多个 CPU,都有自己的 Cache。核心问题是他们如何通信,关键是如何通信,而避免一些锁。
Cache 传播。
Memory 可见性。
Cache Line 对齐
内存屏障
10 通信模型
通信方式
共享内存
简单
需要保护确保一致性
受限:伸缩性,分布式
消息
容易理解
伸缩性好
没有状态依赖
同步和异步
同步
好处编程模型简单
坏处:阻塞,错误级联
异步
需要回调处理,通过 reactive 方式解决
通信结构
集群
master-slave
Real-time Bidding
实时广告计算:
互联网广告领域
竞价
11 数据处理和存储
选型
数据库以及解决方案有很多,还是要跟业务目标和问题特征相匹配。
不能盲目的选择,一定要了解各个维度的特点。
数据模式
清楚的知道数据模型,关系型?Graph?
用面向对象建模其实是 Graph,然后用 ORM 层去适配,我能不能把 Graph 直接存到数据库?
事务等级
数据访问特点
写频繁还是读频繁?是随机查询?
数据量
CAP
log-based 数据结构
解决频繁写。采用顺序写的方式。
12 Uncertainty 不确定性
bug 分为两种
波尔型
海森堡型:
观测的行为会影响本来的行为。
要承认这种 BUG 的存在,并且解决不了。
但是我还要开发出可靠的软件,那怎么办?
Crash Oreinted
Error Kernel
操作系统,一开始没有内核态用户态。
原则
关键数据要保护好
这部分要尽量简单
性能要比较高
隔离(运行时的封装)
13 Design War
需求
一个计算系统,提供:
fib,map,sum 等计算。
提供用户一种语言,用户只要按照这种语言描述出来。
特性:
快。不希望重复计算。
充分利用计算资源,并行化。
可用性
设计
DSL
解释器
计算依赖分析
找到计算资源
分派任务
文档
需要涵盖关键的用例。不仅仅是为了满足用户的需求,而是关键状态的迁移。
需要涵盖动态的,和静态的。
需要涵盖实体的特性,他们之间如何通信的。
需要解释各个设计构造的选择原因,解决的问题。
语言
CDG:
{fib, 5}
{fac, 4}
{sum, [1,2,3]}
{map, fib, [1,2,3]}
{fac, {fib, 5}}
第一步 实现解释器
用例
6> interpreter:parse({fib,5}).
{{fib,5},parallel}
7> interpreter:parse({fac,5}).
{{fac,5},parallel}
8> interpreter:parse({sum,[1,2,3]}).
{{sum,[1,2,3]},parallel}
9> interpreter:parse({map,fib,[1,2,3]}).
[{{fib,1},parallel},{{fib,2},parallel},{{fib,3},parallel}]
10> interpreter:parse({fac,{fib,5}}).
{{fac,{{fib,5},parallel}},parallel}
最复杂的:
12> interpreter:parse({map,sum,[{map,fib,[1,2]},{map,fac,[3,4]}]}).
[{{sum,[{{fib,1},parallel},{{fib,2},parallel}]},parallel},
{{sum,[{{fac,3},parallel},{{fac,4},parallel}]},parallel}]
关键点
语义抽象。
这个是业务核心逻辑的抽象。至于怎么分派,怎么执行是实现层面上的。
评论