写点什么

架构师进阶之《做踏实的架构》

用户头像
陈皓07
关注
发布于: 2021 年 02 月 28 日

做踏实的架构


1 引言


架构

重要性

讨论非常多,各个层面都有

感觉很重要,但是无从下手。

我这样做是否能达成目标,没有很直观的关联。


关键点

我做出来的东西和我想满足的目标,是否有一个明显的关联


什么是架构?


定义非常多:

  1. 系统有很多组成部分,有很多关联,共同协作达成一定的目标。非常形式化的描述,很多系统都可以这样描述。

  2. 哲学观:一切都是对象,一切都是进程。。。。

  3. 系统里面最重要的一些东西。也是一个含糊的东西。。。

  4. 系统里面难以改变的东西。我们希望是推迟决策的。


如何做架构?


瀑布:很长的阶段专门做架构

迭代模型:以迭代的方式,并不是专门的时间专门做架构


结论


无论怎么定义架构,无论怎么去做架构,有一点是一定的:

架构一定是以设计构造的方式体现出来,并且对应你的目标。


另外:

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 数据结构和算法


决定了能不能用,没有可争辩的。作为程序员来说,这个东西最重要,第一个应该学这个,设计应该放到后面。


关键点


  1. 复杂度

  2. Sort,Search

  3. List,Stack,Queue,Hash,Tree,Graph

  4. 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 等计算。


提供用户一种语言,用户只要按照这种语言描述出来。


特性:

  1. 快。不希望重复计算。

  2. 充分利用计算资源,并行化。

  3. 可用性


设计


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}]


关键点

语义抽象。

这个是业务核心逻辑的抽象。至于怎么分派,怎么执行是实现层面上的。


VERSION 1

parse(N) when is_integer(N) ->    N;parse({fib, CDG}) ->    {{fib, parse(CDG)}, parallel};parse({fac, CDG}) ->    {{fac, parse(CDG)}, parallel};parse({sum, L}) ->    {{sum, L}, parallel};parse({map, Op, L}) ->    [{{Op, parse(A)}, parallel} || A <- L].
复制代码


VR


用户头像

陈皓07

关注

还未添加个人签名 2019.04.11 加入

还未添加个人简介

评论

发布
暂无评论
架构师进阶之《做踏实的架构》