同步并发与异步并行

你在那里苦苦等待,等来的却不是你要的结果

最近几天由于市场的疯狂,系统中各种隐藏的问题开始爆发,这些问题或多或少都与同步并发有关。

同步并发系统有两大痼疾

  1. 线程池和连接池是典型同步并发:每收到一个请求,从池中取出一个线程处理业务逻辑,直到业务完成才归还线程。为了提高并发量,线程池中的线程数增加,但是线程数增加,首先占用资源大,其次业务处理过程中需要用同步锁保护数据,同步锁访问导致线程上下文切换,成为并发量的瓶颈。这个瓶颈的存在,由于系统访问量的突发高峰,系统的容量的问题便爆发了。
  2. 更严重的问题是,由于关键数据没有同步引起的并发逻辑问题。当今的系统,几乎都是分布式的,要想在多线程,多进程甚至多服务器并发节点间保证数据同步何其艰难,任何一个数据操作发生竞争,都将导致系统逻辑上出现错误,这种错误如果涉及到交易和账户将导致无法挽回的灾难。

以上两个问题,在市场疯狂的时候同时爆发,这——不仅仅是巧合。解决的方案唯有异步并行。

异步并行的系统,使用自解释、自包含的消息承载数据,每一个节点的业务只需要根据消息内容即可处理,无需再访问共享数据。于是多线程,多进程,多服务器节点间无需同步,各自并行处理。

回调不是真正的异步。回调需要在现场保存状态,既然存在状态就可能由于竞争而导致不一致。只有消息驱动的系统才是真正的异步。

并发模式之比较

前文比较过我们的应用框架和Actor模式,不过感觉仍然不够充分,所以这段时间跟进了一些常见的并发模式,发现了一些本质上的区别。

讲到并发计算我们常常会讲到Scala的Actor,Go的goroutine,以及erlang的process,goroutine遵循CSP(Communication Sequence Process),而Actor多少也受CSP的启发,而且这三者从外面看会有很多相似的地方,但是实际上细究起来还是有所区别的:

  1. Actor之间的通信是异步的,就是说一个Actor将消息扔到另一个Actor的Mailbox以后就不用管了,不用等待另一个Actor处理完。
  2. goroutine是同步的,一个goroutine将消息扔给另一个goroutine处理,并且阻塞等待另一个goroutine处理完,才能继续。只不过,go实现了一套调度机制,可以使得goroutine可以重入而不是真正的阻塞操作系统的线程。
  3. erlang的process也是异步的,不过由于erlang是一种函数式编程语言,所以不存在状态共享,是严格的无副作用。Scala则有一些把函数式编程与命令式编程糅合在一起的味道。

goroutine的同步特性使得用命令式编程写起来非常简单,这也是go受热捧的原因。但是同步的通信毕竟不是一个可以很好的scale的方案,所以go在多核,分布式计算上扩展起来比较困难,相反Actor和erlang的process则轻易的做到location transparency,比如Actor可以运行在集群中的任何一个节点。

我们的应用框架是异步的,这一点和Actor接近,可以做到location transparency,不过有一点与以上三者都不同:

  1. 我们的应用框架中,消息发送者与消费者不产生耦合,发送者只需要发出消息,由消费者去选择感兴趣的消息处理,是一种总线结构。
  2. erlang和Actor中消息发送者需要知道接收者的ID,才能发送消息,是一种点对点结构。Actor也可以通过Channel解耦发送者和接收者,但是本质上每一个Actor仍然需要维护自己的一个入口消息队列。
  3. goroutine使用匿名管道解偶了消息发送者和接收者,但这只是在编译器解耦,也就是不用开发者显示指定接收者ID,运行期一样产生耦合,因为毕竟是同步调用,还是点对点结构。

交易中间件——为何不使用Akka

我们的交易中间件所采用的容器技术与Akka的Actor模式有很大的相似性:

  • Actor之间不共享数据,仅仅通过Immutable的消息通信,这点与我们的基于Disruptor的消息流水线是一致的。
  • Actor支持通过ZeroMQ和PGM作Pub/Sub
  • Actor甚至支持Persistence,Actor之间的通信支持Event Sourcing

既然Actor如此强大,为什么我们不用其作为我们的通信基础设施和应用容器呢?主要原因是两点:

一. 性能

  • 为了达到超高的性能,基于队列的通信满足不了我们的要求,我们需要采用多播的消息总线和Disruptor作为通信手段,这类技术的一个特点是发送者和消费者之间是无耦合的,发送者只需要发送,无需了解有多少个消费者。而Akka的Actor从一开始就构成了一颗树,由父节点管理子节点,这实际上在父子节点间引入耦合,代价就是消息的传递存在额外的负担。
  • 我们的设计要求使用不产生GC的内存管理方法,使用RingBuffer技术很简单就可以做到。而Akka在这一点上是不满足要求的。
  • 我们的架构是非常简单的,Akka的特性远远超出了我们的需求,使用Akka当然可以完成我们的功能,但是如果需要调优的话,很可能需要对Akka进行扩展,Akka这么多特性的实现反而会成为负担。

二. 易维护

Akka的Actor之间的通信是很自由的,Actor也支持动态创建,销毁,生命周期管理,甚至支持软件事务内存,这中间的逻辑实际上可以很复杂。而我们的技术从一开始就着眼于Event Sourcing + 消息流水线 这种简单的通信方式,无需这么多动态特性,无需事务。可以说我们的交互是基于服务的粗粒度的,监控和管理也是基于服务的,因而在实现上可以非常简单,简单的好处是行为可预期,出了问题也很好分析和解决,这对于金融服务来说尤其重要。

Akka的Actor的适用范围比我们的框架要宽广的多,这也恰恰是我们不使用Akka的原因。Akka提供的这么多灵活的特性是其最大的特点,但是对于我们这样的简单架构来说,反而会成为负担。举一个例子吧:

Actor的Persistence实现采用LevelDB,而我们的Event Souring实现仅仅需要通过WAL做到顺序读写就可以了,LevelDB和WAL这两个从性能上和可维护性上比较显然后者要好的多。

基于多播的消息总线——可靠传输

概述

数据的可靠传输是任何通信基础设施必须要考虑的问题。由于数据通过网络传播要经过多种设备和中间件,所以有可能会乱序,或者丢失部分数据。一般做法是发送端将消息排序后发送,接收端按照顺序接收消息。如果发生乱序,接收端负责组装,如果发生消息丢失,发送端重发数据。

发送端不可以任意的往网络上发送数据,必须考虑网络的带宽,以及接收端的消费能力。为了维持发送端和接收端之间的稳定连接,接收端需要发送确认消息给发送端,有两种确认消息的方式,ACK和NAK(Negative ACK),TCP采用的是ACK,而多播则采用NAK。

基于ACK的可靠传输

我们先以TCP为实例看一下基于ACK的可靠传输机制,下图是图例

图例

初始状态,TCP的发送窗口大小为8,下图表示序号为0,1,2的数据已成功发送并得到接收端的确认,这些数据不用重新发送,可以从缓存中删除。而序号3,4已经发送,但还未收到确认,仍然留在缓存中。序号5至10可以发送,应用还未发送。序号11以上超出发送窗口(3-10)的范围,不可发送。

初始状态

如果超过一定时间仍然没有收到3和4的确认,那么滑动窗口向左回退,于是应用重传3,4序号的数据,这便是回退n协议。回退N协议规定即使接收端收到了序列为4的数据,如果没有收到3,则只能将确认序号设为2。

回退N

更高效的做法,接收端收到4立刻,确认4,而发送端在重传时只需重传序号3,这便是选择重传协议(又叫SACK)

选择重传

当发送端收到序号为3的确认之后,窗口向又滑动,序号3可以从缓存中删除,序号11可以发送。

右滑动

TCP之所以采用ACK的方式确认,主要目的是为了拥塞控制,因为Internet跨度大,网络传输各种因素影响较大,等到接收端的ACK之后再发送后续的数据,可以保证发送端不至于发送太多的数据占满带宽或者超出接收端的处理能力。TCP的慢启动和拥塞控制算法也都是基于ACK的,这样做是为了维护一个稳定可靠的互联网,否则很有可能有限的出口带宽被少数几台机器撑满。但是在高速网络上,TCP不够高效的问题就显露出来:

  1. 高速网络上,丢包概率小,太多的ACK反而造成负担,而且发送端要等待ACK回来才能继续发送,会导致一定的时延。
  2. 更主要的问题,多播的情况,一个滑动窗口满足不了多个接收端可靠传输的需要。

基于NAK的可靠传输协议

多播的解决方案是采用更激进的NAK协议,以下也以一个实际的例子解释:

如下图所示,序号0-4是已经发送的数据,而5以后都是可发送数据,窗口没有右边界,发送过的数据也会保留较长时间(甚至落地存储,即Event Sourcing)

NACK1

如果发生丢包,接收端会在超时之后向发送端发送一条序号为3的NAK消息,于是序号3,4重新打开,发送端重发3,4以及以后的数据,这里还是回退N协议。

NACK2

如果有多个接收端同时发送了多条NAK消息,那么有几种处理方式:

  1. 取最小序号的NAK消息,这还是回退N协议
  2. 为每一个发送NAK消息的接收端建立一条单独的窗口,这就是分组回退协议
  3. 接收端的NAK消息指定哪些消息没收到,而不仅仅是一个序号,这样每一个窗口可以做有选择的重传,此即分组选择重传协议

NAK协议更加激进的发送数据,只要没有收到NAK就可以发送数据,因而吞吐和时延比ACK协议会好,在高速网络上,NAK一般会比较少,所以不用担心分组回退会引起吞吐下降。即使最坏的情况,由于采用了多播,也不会比TCP多条连接的情况差。

在出现网络波动的情况下,或者有个别慢接收端的情况下,TCP拥有慢启动和拥塞控制算法,仍然可以保证可靠传输不丢数据。NAK协议也需要有一套相应的拥塞控制和启动算法(未完待续)。

浅谈微服务中的分布式概念

微服务(Micro Service)与SOA都强调通过粗粒度的服务而不是细粒度的远程过程调用达到解耦的目的,但是两者也有不同,主要体现在微服务更加强调去中心化和分布式。

SOA倾向于指定一整套的标准涵盖消息传输,路由,转换、中心化的消息编舞(Orchestration)、业务工作流和规则引擎,试图用中心化的一个平台尝试去解决和描述各种类型的业务问题。可以说,SOA是先有平台标准,然后将业务往已有的平台规范上面套。

微服务则反其道而行之,以业务本身为中心,而通信则通过可重用的库,由业务开发时选择合理的。微服务不注重中心化的Orchestration,而比较青睐轻量级的Choreography,通过简单的ZeroMQ或者restful接口通信。

这两种方式应该说是各有千秋吧,开发能力差,业务规则比较规范的企业可以采用SOA,开发能力强,业务规则不好定制的企业则采用微服务。现在微服务比较火应该是拜云计算所赐吧。另一方面互联网的重视用户体验的思维,靠SOA这种比较厚重的框架去实现,也不现实。

不过实际上多数传统企业都是中心化的管理思路,根据Conway‘s Law,一个企业的组织架构往往决定了该企业研发部门,甚至项目的结构,所以这种去中心化的思维仍然是任重而道远啊。不过现实很骨感,在这个信息化的时代,不是被互联网颠覆,就是被淘汰。

领域建模的目的

在表现层和数据层之间为何需要有一层中间抽象层:领域层?何不表现层直接访问数据层简单直接呢?我想目的主要有:

  1. 领域建模是对业务的抽象,通过封装,继承,组合,分层等各种分析手段,将领域业务理顺,做合理的划分,从而领域建模本身就是将业务清晰的过程。
  2. 领域建模中抽象出来的概念名词,以及代表方法的动词,组成词汇表,有利于业务人员与开发人员沟通。
  3. 领域建模采用面向对象的方法,有利于逻辑的重用,和新业务的扩展。通过多态,能够通过简单的派生新的类型,使得原有的逻辑自动应用到新的业务。
  4. 相比于传统的基于用例的分析,领域建模站在系统本身的内在的角度,而不是系统的外在表象,因而能够分析清楚系统的内在本质,这也是领域建模相比用例驱动的开发扩展性和可维护性更强的原因。

敏捷开发——细粒度提交

敏捷开发鼓励细粒度的更新和提交代码,主要是为了将变化的成本减至最小。如果采用粗粒度更新和提交,有以下问题:

  1. 由于长时间未更新代码,等到更新的时候如果发现代码集成有问题,改动的成本会更大。
  2. 其他提交代码的人已经转到新的任务,这时候再回来做集成打断其任务,而且因为是较长时间以前的代码,其记忆也不是很清晰。

正确的做法是:

  1. 任务对所有人透明,从而集成的时候不用在做额外的沟通
  2. 不用跨部门,可以就地集成
  3. 如果集成遇到问题,因为提交的代码粒度比较细,所以变化引起的改动成本不会太高

二要做到以前几点,团队的组成和实践也应该符合敏捷开发的原则:

  1. 团队的规模不能太大
  2. 故事点和任务的划分清晰,而且足够细

基于Java的金融应用中间件与消息总线

概述

2013年是互联网金融元年。现阶段,互联网在技术上对传统金融系统的冲击主要表现在两方面:

  • 为了支持海量用户,云计算,云存储逐渐替代传统IOE的软件和设备。集中式的架构也向分布式转变,并且允许分布式节点有不一致性的情况。
  • 交易系统对一致性有较高的要求。多资产交易,高频交易等促进了能够充分利用硬件性能的消息处理框架和消息通信框架的产生和发展。

对于云计算在金融系统中的应用已有不少例子,但是对于后一方面,国内相对华尔街等成熟市场来讲相对落后。我们率先在国内研发了一套应用中间件框架和消息总线系统,该系统基于Java开发,并且依托于开源的消息处理框架和消息通信基础设施,支持中间件的敏捷开发和灵活部署。该系统在保证高可用和一致性的前提下,理论上可处理达到百万量级的消息吞吐。

系统架构

传统金融交易系统

金融交易系统,一般分为消息中间件和应用中间件。消息中间件负责消息的路由和转发,而应用中间件则负责消息的流水线处理。传统的金融交易系统一般具有如下的架构

该架构会有以下问题:

  1. 消息中间件(一般采用消息队列实现)是一个中心节点,消息首先发送到消息中间件,消息中间件根据路由配置转发。消息中间件水平扩容难度较大。
  2. 采用集中的数据库存储数据,保证数据一致性。数据库也是一个中心节点,会成为性能瓶颈。
  3. 应用中间件采用线程池处理并发请求。并发将导致不确定性,并且需要引入同步锁机制。
  4. 应用中间件采用冷备份,失效之后备用节点从数据库载入数据,需要较长时间。

我们的解决方案

传统的高性能系统往往使用C/C++开发,而我们的解决方案使用Java开发,这是因为Java在高性能计算领域已经证明了其可行性,而Java的开发效率和庞大的社区支持是C/C++无法比拟的。

我们的解决方案是一种分布式的方案。首先,在网络拓扑结构上,系统具有多个服务器节点,节点通过可靠多播(ZeroMQ + OpenPGM)的消息总线发送和接收消息。所有消息可以达到任何节点,由节点本身而不是消息中间件根据消息主题和消息属性选择感兴趣的消息处理。

新金融系统网络拓扑

其次,节点由多个消息处理器组成的消息流水线构成。同理,消息在处理器之间的传递也是分布式的,每一个处理器根据消息主题和消息属性选择感兴趣的消息。

应用容器消息流水线

相比传统系统,该系统具有以下优势:

  1. 采用消息总线代替消息中间件,消息接收者可任意添加,水平扩容无压力。
  2. 排队机对消息排序,通过Event Sourcing模式保证一致性。采用WAL(Write Ahead Log)持久化,性能远高于一般数据库。
  3. 应用中间件采用开源的LMAX Disruptor模式实现消息流水线,基于通用硬件即可达到超高性能。开发也更简单:无需并发处理,确定性的编程模型,无需同步锁。
  4. 以上两点也保证了互为备份的多个应用中间件状态完全一致,可以做到双活,零延迟失效切换。

设计原则

作为一个典型的实时高性能系统,我们采用了响应式(Reactive)模型。根据参考文献中的The Reactive Manifesto,一个响应式的系统的设计应该关注以下几点原则:

响应事件(Reactive to Events)

一个响应式系统应该是真正异步的,事件驱动的。

  • 我们的系统是一个真正的异步系统,节点/处理器之间不存在共享可变状态,所有的状态变化都通过消息在节点/处理器之间传递,处理器只处理其职责范围内的计算,没有阻塞的同步操作。
  • 这个系统中,事件(消息)是一等公民,系统的设计就是抽象事件,事件处理器,设计事件路由表和事件流水线。事件消息通过消息总线在节点之间高速传递,通过消息流水线在处理器间超高速的传递。

响应伸缩(Reactive to Scale)

为了可伸缩,事件处理器要做到位置透明Location transparency。

  • 在我们系统中,由于事件处理器之间无共享状态,所以事件处理器可以部署到任何一个节点,以及消息流水线的任何位置,要做的只是配置好消息路由。这保证了系统的向上伸缩(Scale up)以及向外伸缩(Scale out)。向外伸缩,只需将新服务器节点接入总线多播组,而向上伸缩,只需增加处理器的数量,并且把消息按序号进行划分。

响应错误恢复(Reactive to Resilience)

响应式系统应当是鲁棒的,能够自动处理错误的。

  • 在我们系统中,应用中的错误也作为一种事件在节点和处理器之间流动,错误消息也有其自有的主题和消息类型,任何一个节点/处理器在配置路由时可以选择关心的错误消息,从而能够成为一个Supervisor角色。这个系统是否鲁棒,取决于Supervisor是否能够很好的处理错误,而发生错误的处理器不用现场处理。这实际上将应用逻辑与处理失败的逻辑隔离开来,有利于业务逻辑相对干净,而Supervisor角色则专注于治愈错误,防止错误在节点/处理器之间传递和扩散。

响应用户(Reactive to User)

以上几点决定了系统可以实时响应用户操作,以此为基础可以产生丰富的用户交互模型和友好的用户体验。

性能

系统唯一的中心节点是排队机,因此排队机的性能也就直接决定了系统的性能。以下是对排队机的压力测试结果。

排队机性能测试结果

测试使用的是两核八线程CPU,通过千兆网卡与交换机连接。测试的最高吞吐可达到35万/每秒。分析表明性能瓶颈主要在网络,使用虚拟网络接收测试表明,排队机的吞吐量可达到接近百万/每秒。接下来优化的方向主要有:

  1. 使用万兆网卡,或者更高速的InfinityBand解决方案
  2. 使用自主研发的更轻量级的可靠多播协议代替OpenPGM

应用

该系统逐步应用于交易,风控,做市策略系统。以下以策略系统的消息流水线配置为例其阐述系统的应用。

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
<stage plugin="bus" processor="recv"/>
<stage plugin="queue" processor="removeDuplication">
    <filter reverse="true"><topic>sys</topic></filter>
</stage>
<stage plugin="monitor" processor="default">
    <filter><topic>sys.monitor</topic></filter>
</stage>
<stage>
    <processor plugin="hsuf" processor="byteToHsuf">
        <filter><topic>strategy.server</topic></filter>
    </processor>
    <processor plugin="strategy" processor="byteToStrategy">
        <filter><topic>strategy.command</topic></filter>
    </processor>
    <processor plugin="translator" processor="byteToJson">
        <filter><topic>quote</topic></filter>
    </processor>
</stage>
<stage plugin="strategy" processor="management" env="strategy">
    <filter><topic>strategy.command</topic></filter>
</stage>
<stage sequential="false">
    <processor plugin="strategy" processor="runner" env="strategy">
        <filter reverse="true"><topic>sys</topic></filter>
    </processor>
</stage>
<stage>
    <processor plugin="hsuf" processor="hsufToByte">
        <filter><topic>hsuf</topic></filter>
    </processor>
    <processor plugin="strategy" processor="strategyToByte">
        <filter><topic>conn</topic></filter>
    </processor>
</stage>
<stage plugin="bus" processor="send"/>

结论

基于开源消息处理和通信框架,基于Java平台研发的分布式交易中间件和消息总线系统,在架构上和技术上都是可行的,该系统使用通用硬件即可达到数据一致性,可用性,超高性能,可伸缩和可扩展。

参考资料

  1. The Reactive Manifesto
  2. Disruptor Technical Paper
  3. Mechanical sympathy
  4. ZeroMQ
  5. Event sourcing

响应式编程(Reactive Programming):事件驱动

CPU多核架构的流行,以及Node.js等全异步软件平台的出现使得异步编程逐渐走上主流舞台,相应的各种异步框架也逐渐出现

回调(Callback)

当我们说到异步的时候,自然而然的就想到Callback,函数调用的结果通过Callback回调返回,在结果返回之前,线程可以继续运行执行其他的操作而不用阻塞。

非常完美,是吗?但是当多个异步操作具有依赖性时,怎么办,这就产生了异步编程中常说的Callback Hell问题,多个Callback嵌套导致代码可读性很差。

  1. 代码缩进非常难看。
  2. 回调代码分散在各处,来回跳跃,流程很不清晰。
  3. 状态分散在多个closure中,难于管理。

代码可读性差只是表现,深层次的原因还是系统建模方法不对。使用Callback就和面向过程一样,将重心放在了操作本身,只能看到眼前,而不能在一个更高的层次上去纵观整个系统。

Future和Promise

Future在Callback的基础上将回调对象化。Future与Callback的区别在于Future引入了完成,未决,错误等标准回调事件,在Future状态改变时以事件的方式通知关注者,这些事件本身就是对象。于是Future将设计者的重点从操作向事件建模方向转变。

事件建模之外,事件在系统中的流动也需要建模,于是就有了Promise,可以将多个事件处理器串联起来,多个依赖的异步操作转化成事件流程的声明,从而一眼就能从代码中看出依赖关系。

Reactive Extension (RX)

RX在Future和Promise的基础上更进了一步,将单一的事件处理扩展到多个先后相关的事件流处理。举个例子,鼠标拖拽事件,是由一个MouseDown事件加多个MouseMove事件以及一个MouseUp事件构成,Promise处理这种情况需要处理器具有状态记住拖拽的阶段。RX将MouseDown和MouseUp这些事件的处理标准化,并且将拖拽阶段的这一共享状态从业务处理器中抽离,而固化到事件处理流程中。RX抽象了大量的事件操作,使得我们可以将重心放到事件流程建模中,而不是具体的一个接一个事件的处理。共享状态从处理器中抽离也有利于业务处理器的重用以及并发处理。

综上,响应式编程中的事件驱动,要求

  1. 对事件建模
  2. 对事件流程建模
  3. 对事件相关性建模

软件开发之道——程序员的坏习惯

近两个月兼任移动与桌面客户端的架构工作,与以前喜好单打独斗的程序员们打交道的过程中,发现一些程序员常有的一些不好的习惯

不重视单元测试,对测试有误解

开发工程师倾向于完成功能之后,立即交付测试工程师手动测试或者上层调用者测试,测试有问题再打回来修改bug,如果要求交付之前写单元测试,则认为是在阻碍功能交付的进度,仿佛“任何阻碍我尽快的完成功能开发的行为都是与我有不共戴天之仇”。

表面上写单元测试是多花了时间,实际上等发现bug再打回来重写,这中间产生的沟通成本要大得多,于是出现这样的情况:两个星期把一系列功能完成,而真正稳定可发布则要两个月时间,绝大部分时间耗费在测试,修改bug这种来回的扯皮中。

敏捷开发认为单元测试是成本最小的,一个bug在单元测试阶段发现的成本比起功能测试时才发现要小得多。单元测试虽然是一种白盒测试,但是测试点仍然是对象的接口,白盒主要体现在依赖注入上,单元测试的过程本身就是验证接口设计的过程,甚至在TDD里设计本身就是由测试驱动的。单元测试可以细粒度的检测bug,可以把因素锁定在有限的范围内,并最快的速度迭代,比起功能开发完再测试,成本要小一个量级。我们希望看到的是可以持续的交付,两个星期一个迭代完成功能,同时测试也通过。

合理的对象建模,面向接口的设计,Mock注入,Expect框架等测试自动化框架和工具的使用,可以有效的提高测试的效率。

害怕变化

在多人协作,模块化开发中,上层应用的开发者希望下层模块把接口提前设计的完美,接口定了之后最好不要有任何变化,否则改动的影响可能会非常大。这个要求其实也无可厚非,但是敏捷开发告诉我们,这种思路是太理想化的,接口也需要迭代过程中不断完善,需求和架构都是在迭代过程中逐渐清晰的。我们设计领域和事件模型,采纳MVC框架,模块化和层次结构设计,依赖注入等等,无非是为了一点,变化的时候改动的成本尽可能小。快速的迭代,细粒度的重构也是减小变化成本的必须。

程序员不仅不要害怕变化,而且要带着积极的心态拥抱变化。

缺乏安全感

很多程序员不喜欢讨论内部的设计,而喜欢对外的API接口设计。他们认为如果太过透明,一是侮辱自己的智商,二是影响自己在项目中的不可或缺性,最好把自己的实现功能作为一个黑盒子给别人,只要自己能够最快的把这个黑盒子实现,就是个人能力的体现。这实在是很短视的一种思想,不过考虑到国内软件企业的现状,倒也情有可原,因为最终的考核都是基于工作量和在项目中的不可或缺性。程序员应该有更高的追求,作为一个团队的一员,以科学的方法论指导工作,保证促进团队的整体效率。而作为个人考核的度量则是新的技术解决难题,引进了新的方法提高了效率等,只有不断进步才能体现个人价值。