技术

mosn有的没的 负载均衡泛谈 《Mysql实战45讲》笔记 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go细节 codereview mat使用 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes垂直扩缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 《阿里巴巴云原生实践15讲》笔记 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 容器日志采集 Kubernetes 控制器模型 Kubernetes监控 容器狂占cpu怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 硬件对软件设计的影响 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM内存与执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go学习 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 kubernetes crd 及kubebuilder学习 pv与pvc实现 csi学习 client-go学习 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 《推荐系统36式》笔记 资源调度泛谈 系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 《聊聊架构》 书评的笔记 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


处理复杂性

2019年08月14日

简介

本文主要来自对阿里巴巴高级技术专家张建飞 公众号文章《从码农到工匠》的梳理和总结。作为技术人员,我们不能忘记我们技术人的首要技术使命是治理软件复杂度。

如何定义复杂性

在《人月神话》里,作者把复杂性分为两种:

  1. 本质复杂性(Essential Complexity),指的是你要解决的问题本身的复杂性,是无法避免的。
  2. 附属复杂性(Accidental Complexity),是指我们在解决本质问题时,所采用的解决方案而引入的复杂性。在我们现在的系统中,90% 的工作量都是用来解决附属复杂性的。

比如做一个电商系统,成本是多少?可能几千块,也可能很多亿。如果你理解这个的答案,那意味着比较理解当前软件编程的复杂性问题。因为软件系统的复杂性会随着规模急剧上升。

降低软件复杂性的一般原则和方法 斯坦福教授、Tcl语言发明者John Ousterhout 的著作《A Philosophy of Software Design》

\[C=\sum_{p}c_pt_p\]

子模块的复杂度Cp乘以该模块对应的开发时间权重值tp,累加后得到系统的整体复杂度C。也就是说,即使某个模块非常复杂,如果很少使用或修改,也不会对系统的整体复杂度造成大的影响。子模块的复杂度Cp是一个经验值,它关注几个现象:

  1. 修改扩散,修改时有连锁反应。
  2. 认知负担,开发人员需要多长时间来理解功能模块。
  3. 不可知(Unknown Unknowns),开发人员在接到任务时,不知道从哪里入手。

互联网行业的软件系统,很难一开始就做出完美的设计,通过一个个功能模块衍生迭代,系统才会逐步成型。对于现存的系统,也很难通过一个大动作,一劳永逸地解决所有问题。系统设计是需要持续投入的工作,通过细节的积累,最终得到一个完善的系统。因此,好的设计是日拱一卒的结果,在日常工作中要重视设计和细节的改进

组织层面

技术人自己的KPI

  1. 不管你们有多敬业,加多少班,在面对烂系统时,你任然会寸步难行,因为你大部分的精力不是在开发需求,还是在应对混乱。造成这种局面,我们的技术管理者,我们的TL要负有主要责任。说的重一点,是工作上的失职,这种失职主要体现在两个方面,一个是技术不作为,另一个是业务不思考。
  2. 技术人员的疲于奔命,内因上是由于上面分析的团队技术味道的缺失,外因上主要是PD的乱作为。

架构设计

阿里研究员:警惕软件复杂度困局软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。越是大型系统,越需要简单性。对于真正重要的、长生命周期的软件演进,我们需要做到对于复杂度增量零容忍。

架构这个词似乎蕴含了一种建造和设计的意味。一个摩天大楼无论多么复杂,都是事先可以根据设计出完整详尽的图纸,按图准确施工,保证质量就能建造出来的。然而现实中的大型软件系统,却不是这么建造出来的。软件是长出来的。大型软件设计核心要素是控制复杂度。这一点非常有挑战,根本原因在于软件不是机械活动的组合,不能在事先通过精心的“架构设计”规避复杂度失控的风险:相同的架构图/蓝图,可以长出完完全全不同的软件来。所以说了这么多是要停留在形而上吗?并不是。我们的结论是,软件架构师最重要的工作不是设计软件的结构,而是通过API,团队设计准则和对细节的关注,控制软件复杂度的增长

软件复杂度从根本上说可以说是一个主观指标,说其主观是因为软件复杂度只有在程序员需要更新、维护、排查问题的时候才有意义。复杂度指的是软件中那些让人理解和修改维护的困难程度。相应的,简单性,就是让理解和维护代码更容易的要素。因此我们将软件的复杂度分解为两个维度,都和人理解与维护软件的成本相关:认知负荷与协同成本

  1. 认知负荷 cognitive load :理解软件的接口、设计或者实现所需要的心智负担。
    1. 定义新的概念带来认知负荷,而这种认知负荷与 概念和物理世界的关联程度相关。
    2. 逻辑符合思维习惯程度:正反逻辑差异,逻辑嵌套和独立原子化组合。继承和组装差异。
    3. 接口设计不当,比如暴露出去的方法有一些隐含约定,进而导致使用不当。也比如 有多种方式让调用者实现完全相同的功能
    4. 一个简单的修改需要在多处更新,命名(Naming的难度在于对于模型的深入思考和抽象,而这往往确实是很难的。)
  2. 协同成本Collaboration cost:团队维护软件时需要在协同上额外付出的成本。
    1. 增加一个新的特性往往需要多个工程师协同配合,甚至多个团队协同配合
    2. 测试以及上线需要协调同步。交付给其他团队(包括测试团队)的代码应该包含充分的单元测试,具备良好的封装和接口描述,易于被集成测试的。然而因为 单测不足/模块测试不足,带来的集成阶段的复杂度升高、失败率和返工率的升高,都极大的增加了协同的成本。

真正的工程师一定在意自己的作品:我们的作品就是我们的代码。工匠精神是对每个工程师的要求。

分模块

Unix操作系统文件I/O是典型的深模块,以Open函数为例,接口接受文件名为参数,返回文件描述符。但是这个接口的背后,是几百行的实现代码,用来处理文件存储、权限控制、并发控制、存储介质等等,这些对用户是不可见的。

与深模块相对的是浅模块(Shallow Module),功能简单,接口复杂。通常情况下,浅模块无助于解决复杂性。因为他们提供的收益(功能)被学习和使用成本抵消了。以Java I/O为例,从I/O中读取对象时,需要同时创建三个对象FileInputStream、BufferedInputStream、ObjectInputStream,其中前两个创建后不会被直接使用,这就给开发人员造成了额外的负担。默认情况下,开发人员无需感知到BufferedInputStream,缓冲功能有助于改善文件I/O性能,是个很有用的特性,可以合并到文件I/O对象里。假如我们想放弃缓冲功能,文件I/O也可以设计成提供对应的定制选项。

业务逻辑和技术细节的分离

应用架构之道:分离业务逻辑和技术细节架构始于建筑,是因为人类发展(原始人自给自足住在树上,也就不需要架构),分工协作的需要,将目标系统按某个原则进行切分,切分的原则,是要便于不同的角色进行并行工作

作为架构师,我们最重要的价值应该是“化繁为简”。架构师的工作就是要努力训练自己的思维,用它去理解复杂的系统,通过合理的分解和抽象,使那些系统不再那么难懂。

六边形架构、洋葱圈架构、以及COLA架构的核心职责就是要做核心业务逻辑和技术细节的分离和解耦。

业务逻辑抽象

按照Wikipedia上的解释,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个 皮质的足球 ,我们可以过滤它的质料等信息,得到更一般性的概念,也就是 球 。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。

OOP可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。

当发现有些东西就是不能归到一个类别中时,我们应该怎么办呢?此时,我们可以通过拔高一个抽象层次的方式,让它们在更高抽象层次上产生逻辑关系。比如,你可以合乎逻辑地将苹果和梨归类概括为水果,也可以将桌子和椅子归类概括为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”。

在程序设计中,也是一样,如果在一个类或者一个函数中涉及过多的内容和概念,我们大脑也会显得不知所措,会觉得很复杂,不能理解。将一个大方法,按照逻辑关系,整理成一组更高层次的小而内聚的子程序的集合,那么整个代码逻辑就会呈现出完全不一样的风貌,显得干净、容易理解的多。

分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。

你写的代码是别人的噩梦吗?从领域建模的必要性谈起

写复杂业务逻辑的方法论

过程分解和对象建模相结合

”能力下沉“

阿里高级技术专家方法论:如何写复杂业务代码?

一般来说实践DDD有两个过程:

  1. 套概念阶段:了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bounded Context,Repository等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
  2. 融会贯通阶段:术语已经不再重要,理解DDD的本质是统一语言、边界划分和面向对象分析的方法。

指导下沉有两个关键指标:

  1. 复用性,复用性是告诉我们When(什么时候该下沉了),即有重复代码的时候
  2. 内聚性,内聚性是告诉我们How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上(因为Domain层的能力也是有两个层次的,一个是Domain Service这是相对比较粗的粒度,另一个是Domain的Model这个是最细粒度的复用)。

矩阵思维

面对复杂业务,if-else coder 如何升级?业务的差异性是if-else的根源。要如何消除这些讨厌的if-else呢?我们可以考虑以下两种方式:

  1. 多态扩展:利用面向对象的多态特性,实现代码的复用和扩展。
  2. 代码分离:对不同的场景,使用不同的流程代码实现。这样很清晰,但是可维护性不好。

结构化思维有用、很有用、非常有用,只是它更多关注的是单向维度的事情。比如我要拆解业务流程,我要分解老板给我的工作安排,我要梳理测试用例,都是单向维度的。而复杂性,通常不仅仅是一个维度上的复杂,而是在多个维度上的交叉复杂性。当问题涉及的要素比较多,彼此关联关系很复杂的时候,两个维度肯定会比一个维度要来的清晰,这也是为什么说矩阵思维是比结构化思维更高层次的思维方式。

除了工作,生活中也到处可见多维思考的重要性。比如,我们说浪费可耻,应该把盘子舔的很干净,岂不知加上时间维度之后,你当前的舔盘,后面可能要耗费更多的资源和精力去减肥,反而会造成更大的浪费。我们说代码写的丑陋,是因为要“快速”支撑业务,加上时间维度之后,这种临时的妥协,换来的是意想不到的bug,线上故障,以及无止尽的996。简单的思考是“点”状的,比如舔盘、代码堆砌就是当下的“点”;好一点的思考是“线”状,加上时间线之后,不难看出“点”是有问题的;再全面一些的思考是“面”(二维);更体系化的思考是“体”(三维);比如,RFM模型就是一个很不错的三维模型。可惜的是,在表达上,我们人类只能在二维的空间里去模拟三维,否则四维可能会更加有用。

我们在做矩阵分析的时候,纵轴可以选择使用业务场景,横轴是备选维度,可以是受场景影响的业务流程(如文章中的商品流程矩阵图),也可以是受场景影响的业务属性(如文章中的订单组成要素矩阵图),或者任何其它不同性质的“东西”。

“业务理解–>领域建模–>流程分解–>多维分析”是体力,是因为实现它们就像是在做填空题,只要你愿意花时间,再复杂的业务都可以按部就班的清晰起来。PS: 有了思维模型,就是在做填空题

《能力陷阱》

发展人际关系让你觉得卑鄙,虚伪?当我们把人际关系定义为本质上是为了实现自我利益时,甚至有些肮脏时,会限制我们的领导力,限制我们的发展前进,限制我们扩展视野,会阻碍我们了解新想法、发展自己其他方面才能的机会。

Be true to yourself or Shape-shifters (”坚持做自己”,还是”随机应变“)? 对于那些“坚持做自己”的人,作者说你要先知道那个You的定义是什么,是过去的“你”,先现在的“你”,还是未来的“你”呢?是那个固步自封,不敢改变的你,还是那个越来越圆融,知道如何应对变化,可以随机应变的你呢? 所以,坚持做自己很多时候也是自己欺骗自己,不敢跳出舒适圈,不敢做出改变的谎言。Shape-shifters(随机应变者)是指那些很自然可以适应新环境的人,他们并不会产生一种觉得自己很虚假的内疚感。“随机应变者”有一个核心的自我价值观和目标,他们不担心转变自己会对自己的信仰造成影响。

时间是有限的,越是在最忙的时候,越需要空出一些时间来思考做这些事情的原因和目的,不能一直闷着头做。时间安排好,并不是一件特别困难的事。领导者,应该把注意点放在一些非常重要的事情上。然后,其它的时间都要用来自我提升,而不是那些“没有回报的事情”。就是要多花时间在自我提升(学习,写作,分享)上,多花时间对外产生连接,拓展人际网络,扩大影响力,吸引更多人才,从而为团队和组织带来更大贡献