基本架构原则

我们都经历过许多架构的选择,那么我们一定也知道,没有最好的架构,只有最合适的架构。面对这么多选择,我们应当遵循什么样的原则?从设计模式到DDD,有一系列的方法论,但我认为有一些思想是通用的,以下是我的一些理解。

预测与快速响应

我们常常信誓旦旦的说,我们的系统是为未来多少年设计的,那这是如何支撑的呢?

如果能提前一步预测到未来,未来的技术趋势,未来的业务变化,未来的流量,哪怕就是眼下的未来,对技术人员来说,应对起来就变的游刃有余。所以我们的架构常常会为这种预测提前做出设计,预测什么非常重要,预测商业的变化不仅对能力的挑战非常高而且准确性低,所以前瞻首先考虑的不是商业未来的变化,而是未来业务完整的形态,因为从另一个视角看商业的变化,其实它是一个业务完善的过程。完整的视角再看当下的业务,也许当下的业务形态刚刚起步,但它一定是未来的基础,而有些未来的重要模块也可能提前被抽象出来,但当下可能仅仅是空实现。

但是,未来真的会延续这种方向发展吗?这就是上面提到的,对技术和业务的理解是主观的,主观就存在不确定性,如何应对这种不确定性?从另一个角度思考,支撑前瞻的另一个重要的原则就是快速响应。快速响应的架构的基本形态就是原子性和配置性。原子是对业务的抽象,代表更稳定业务形态,而配置性是对商业规则的抽象,是对原子能力的编排。原子能力很少变化,业务变化的是商业规则,复用原子业务并利用编排能力快速调整商业规则,这就是快速响应的原理。这种架构最关键的在于原子的抽象,响应未来的变化更多的却是来自对过去的思考,更稳定及复用的抽象就是业务本质的抽象,剥离商业规则,回归到像教科书上的原理一样的本质。

图灵完备

我们常常会用不够灵活,不支持我们的场景来批评一些架构,而为何我们很少这样批评我们的编程语言呢?不同的语言有不同的语法,但我们任何逻辑都可以用这些语法实现。这些语言的基本原则很类似,他们都有一个非常低级的指令集,但他是图灵完备的,换句话说也许它使用起来不方便,但没有屏蔽你和计算机的交互能力;在这些指令之上构建了高级语法,使用者更直观和便捷。

再回过来看架构,缺乏灵活性的架构往往是从业务场景开始分解产生的,先对场景建模,再提取可复用性产生底层模型。而我认为高扩展的架构的设计应当是相反的,最先要设计的是对业务亚原子模型的抽象,所谓亚原子模型,就是比业务最小单元更小更纯粹,但是它要具备另一个特性,就是逻辑的完备性,也就是这种抽象要能支撑对逻辑语法的表达,它不一定要具备图灵完备,但至少在这个业务领域是完备的,事实上越底层的抽象也越容易接近图灵完备。业务的模型也就是这些原子模型的组装或重载,而商业又是对业务模型的组装或重载,商业的变化一部分是复用业务模型实现的,而现有业务模型不支持部分可以扩展业务模型实现,由于最底层的模型具备完备性,这些扩展是建立在不改变整体架构上的。

用一个例子来解释,比如对流程引擎的模型抽象,这个模型不是业务上的任务、服务、分支、并行,而是更纯粹的节点和连接,基于节点和连接的模型,扩展出以上的业务模型,再提供编排能力。

总结一下就是,高扩展的架构就是架构的本身就是由具备逻辑完备性的内核扩展出的。

监控与容错

我们在上线之间有许多保障措施,Code Review,严格的测试,自动化回归,我们尽可能的让我们的系统稳定的运行,但永远没有百分之百的稳定,架构可以更大程度的提升和应对这些问题。

首先要考虑的就是应对环境故障的可恢复的能力,重启和回滚是最常用的两种方式,但它并不像表面看上去那么轻松,应用数量越多,理论的难度越大。环境故障的可恢复的能力影响最大的因素应该是应用的依赖复杂度,分层的设计可能解决一部分循环依赖的问题,但是还有一个原则需要考虑的就是应用的独立性。独立性并不代表没有依赖,而是对依赖的容忍度,Lazy-Load是常用的一种解决方案。

除了坏境故障,我认为优化要考虑的就是快速定位与排查的能力,日志是一种工具,而架构上设计的应该是数据可回溯的能力。从业务数据上看有两类数据,一类是代表现在的当前态数据,如,商品,库存,订单,还有一类是记录过程的过程数据,如,操作记录,资金流水。很多业务模式其实只需要当前态的数据,所以很多架构也只设计了对当前态数据的持久化,但我认为可回溯的架构更应该持久化的应该是过程数据,当前态的数据本质是过程的推倒。这程过程数据的价值在于它可以让你随时的复现每一个时刻。

然后考虑的是容错能力,容错能力不仅仅是指出现异常后可以继续进行,还要考虑异常对数据的污染,因为这可能会将异常走向到不可恢复异常。容错不是在避免异常,而是对已知的但不可避免的异常的兼容处理。最常见也是最被忽略的就是超时与并发。

优化空间

优化无止境是任何技术人员的追求,但性能是个无底洞,架构不一定能解决所有性能问题,但是合理的架构设计,性能应该不会成为瓶颈。排除代码的Bug,我认为性能最主要的问题集中在两点:

首先是重复调用,我认为大部分的性能开销可能就是重复的开销,而查询的重复开销又是最常遇到的,更细粒度的模块划分和微服务可能将这种问题放大化。架构上规避重复查询开销的方式是对会话级别的上下文(Context)的设计,合理的使用上下文传递重复查询的结果。

然后是不合理的资源分配,性能的开销是一种动态的过程,和代码不同无法在开发期就能计算出来。流式计算、虚拟化、弹性、反应式这些都是解决方案,架构上要考虑的我认为是减少压力传递,RPC一种典型的压力传递的方式,整条调用链是同比分压的,所以在某些调用上,我们需要引入另外一些非阻塞模式却断压力的传递,这样才能为弹性扩容留下空间。