来,一起看看import *到底怎么了?

Google Java编程规范》里有这么一条:

3.3.1 import不要使用通配符

即,不要出现类似这样的import语句:import java.util.*;

不少公司也的确是这么要求的。但是当我面试时问起为什么不要这么用的时候,不少人就开始语焉不详了。

其实稍微有点儿心的人,稍微花个几分钟,就可以从网上搜到这个问题的答题。诸如:

  • 会导致不同包的同名类之间会有冲突。比如java.util.Date和java.sql.Date
  • 使用具体的完整的类名,每个类的来源会更明确,代码会更好读。
  • 编译速度更快。

更有意思的讨论,可以移步到这里观战。

然而我写这篇文章,并不是为了解释为什么不要用import *

恰恰相反,我想说的是:上面说的这些不要用import *的原因,听上去是有一定道理,但是其实全是放屁。

  • 对于类名冲突的问题。要考虑是代码的分层是否合理?类的命名是否合理。自己把一个类名叫Thread,还要和处理多线程的代码放一起用,不去反思自己类名是不是起得太混蛋,代码分层是是不是太混乱,然后还跑去怪编译器处理不了。不知道是谁给你这么厚的脸皮的?不拿去磨刀真是可惜了。
  • 在这个AI都叫嚣着要写代码的年代,你还把各种IDE当记事本用,别人写个import *,就说自己找不到这个类是在哪儿定义的,还说这代码不好读。想用代码行数算工资就直说,不用这么拐弯抹角的。
  • 先去把测试代码里的Thread.sleep(1000)处理干净啊。就现在这些工业代码,还不需要计较import *与否造成的性能差异。你真要计较这些,也要先测试一下自己码代码的手速。1分钟之内没办法把26个英文字母打10遍,按你的逻辑就是,你的手速太影响开发效率了,不适合当程序员。麻溜的,主动辞了吧。

最后,再补充一下。我也不是在说import *,我是在说所有的这些,不过脑子,不讲道理或是讲着歪理,然后单纯就是这么做着的,一切屁事儿。

服务配置管理的一些误会

这里指的配置,泛指一切能改变服务行为的变量。其生效的位置可以在编译期、部署过程中、运行期;其存在的形式,可以是代码中,本地文件,资源文件,环境变量,数据库甚至其它服务。也正是因为这种灵活性。配置管理大概算是软件工程里人们分歧最严重的领域。而且,由于“反正无论怎么样搞,怎么样都能搞定。搞定不就行了吗?”的“结果导向”的态度的存在,这部分也常常最不受实质上的重视和控制,于是对配置项肆意随性的使用,最终将导致配置混乱,配置漂移,配置变更风暴乃至生产事故的发生。(顺便说下,我这里当然就是在曲解“结果导向”,但是我见过无数人在想说一个实现细节不重要的时候,便把“结果导向”搬出来说事儿了,所以这里说是“结果导向”是为了便于一些人理解。)

服务配管管理这个话题非常大,一篇文章显然说不完。下面我仅仅讨论几个我感觉人们误会最深的点。主要内容与理念来自《Continous Delivery》一书,但是重点不同。

代码与配置分离

“代码与配置分离”常常被从字面上理解错,从而产生两个严重的误会:

  1. 代码中,不要出现与配置文件有关的任何东西。
  2. 另外,哪部分算“代码”呢?我们要求高些,就整个代码库吧。

于是在犯了这两个误会的代码库中,连个完整的配置文件都找不到。于是你连这个服务都有哪些配置项都看不出来,要去实际环境中,把环境变量啦,本地配置文件啦统统拿出来过一遍才搞得清楚。我遇到过的,更极端的做法是样的:写一个服务维护各个环境的配置项,这个还不错,但是呢,又在CI中加入一个自定义的步骤,把已经打好的Jar包解开,把相关配置下载下来,替换掉里面的application.properties文件,然后重新封成Jar包。我一开始还以为有什么高深的不为人知的原因才这么搞的。后来我发现我想多了,他们就是不想用或是不会用Spring的Profile机制罢了。虽然所有项目都的确是“用了”Spring Boot框架的。但是真的只是用了个框架,本质上这个项目连Spring 4的功能都没用上。

再说回原题,我不想解释“代码与配置分离”是什么意思,因为这只是个处理方式,我想介绍一下这个处理方式要解决的问题是什么。它要解决的主要问题之一是:同一份代码产生的包,能够不经修改地,在不同的环境下运行,配置可以在代码之外进行控制。避免出现“因为生产的配置不一样,所以要改下代码,再打个包再发到生产”的问题。那为什么临时改下,再打个包再发生产不行呢?我乐意加班不行吗?不行,问题在于:

  • 效率低。你改下代码,再打包,要不要再测试呢?即使不要,也10来分钟过去了。能不花额外时间的事儿,就不要花1秒额外时间。加班要创造价值,不是创造问题。
  • 风险高。你说你只改配置,谁知道是不是真的呢?要不要人来检查呢?你手改的怎么保证不出错呢?
  • 成本高。这是上面两条的推论。

其实《CD》这本书,全书基本都是在说提高效率和降低风险。抓住这个主旨,再去理解“代码与配置分离”,至少不会跑得太偏,也不会纠结于怎么分,分到什么程度这种问题。

虚拟化场景下的代码与配置分离

到了虚拟化的环境,情况会略有不同,部署的单位是镜像,那么问题来了,镜像里面应该是纯代码?还是代码与配置的合体?这个复杂些,两个说法都不对。

镜像本身是包含运行环境的,比如OS,JDK,一部分环境变量什么的。所以环境相关的配置,好多都是和OS, JDK相关的,本就应该放在镜像内部,不允许随意从外部修改的。非要从镜像里拿出来,当然技术上是完全可行的,但是没有意义和价值,而且效率会变低。拿出来统一管理所有配置?不不,统一是手段,不是目的。餐具有筷子和勺子,是为了吃饭方便高效,但是吃一碗汤面,不需要为了用筷子和勺子,就非把面条单独捞出来放一个碗。这叫脱了裤子放屁。 配置项到底是在镜像里还是镜像外只是手段。你要看你的问题解决了没有,目的有没有达成。而且,配置放镜像里,就无法统一管理了吗?就无法从外面控制了吗?

当然,还有一些配置,比如密码,密钥什么的,安全起见,就不可以放在在镜像内,加密过的也不行。否则镜像一旦泄漏,别人直接就能把你的服务原模原样地跑起来,这个风险是不可承受的。

所以总结一下:镜像里应该要包含并固化所有环境的多数配置,通过一个参数决定运行哪套配置,少数有特殊要求的配置要独立出来。但是建议保持从镜像外部改变现有配置项的值的能力。以便可以在不重新构建的情况下,直接调整运行时参数来解决一些比较紧急的问题。但是尤其要注意的是,所有这些紧急调整的参数,一定要签入代码库以固化到镜像中。

注意:上述这些行为,Spring通通自带支持,默认就这样,你不需要为这些事儿写一行代码。除非你放屁要脱裤子,那是要写不少代码来支持的。

配置应该放哪里

能放配置项的值的地方非常多,我们先来看下都有哪些地方可以放置配置项及值。

  • 代码里。
  • Jar包的resources。比如:application-xxx.properties文件等。
  • Docker file
  • Host机上挂载的文件
  • K8s的配置文件
  • Host机上环境变量
  • Java, Docker, K8S等的启动参数
  • 各种云平台自身中。如Aliyun, OpenShift

正是因为可以配置的地方太多了,所以必须严格控制好“应该”在哪儿放配置,否则非常容易失控。在没有控制的情况下,多数人会选择自己已经知道的方式或是最方便的方式去做配置变更,而不是去探求最合理的方式去做配置变更。
所以我们先讨论一下,配置有哪几类,分类的维度会有如下几种:

  • 谁来决定值是什么?开发,产品,运维?
  • 要不要随时修改? 实时生效?
  • 是否敏感(需要保密或脱敏)?
  • 是否因环境而不同?
  • 是否有足够的权限。比如你需要有一个Kafka的Topic,这个可以是一种配置,也可以自动化配置,但是如果运维不给你权限的话,你配置这个东西的方式会有极大的不同。

上面并不是所有的维度,但是仅仅上面的维度就有2^4*N种情况,N是公司需要做配置的角色数。当然,你并不需要2^4*N种配置的方式,配置的方式是越少越好。但是要能满足以上所有情况。并且,还要满足以下对配置管理的要求:

  • 版本控制。是的,配置本身也要有版本控制。不是随手改的。至于放在代码之外,或与代码放一起都可以。如果你不想搞得太复杂,就和代码放一起。放在代码之外的话,你还要管理好配置的版本与代码的版本的兼容的对应关系。有了版本,也好做回滚。版本控制的另一个潜台词是配置共享的,团队新来一个成员,应该是可以用得到这些配置,并一键把服务按配置好的参数跑起来的。不需要满世界问人,有哪些参数要配置啊?要配置成什么值啊?你把值拷我一下啊?
  • 统一配置的能力。比如Kafka,常常是多个服务都要配置这么个地址(或ZK的,一个意思)。那你是感觉每个服务各自配置自己的好呢?还是用全公司统一的一个配置好呢?(假设公司只有一套Kafka)
  • 抽象配置存储方式的能力。比如密码,你本地环境的,开发环境的密码,可能会感觉放properties文件就好了。生产的是要通过环境变量的。然后有一天,公司打算用Vault了。你的代码,你的配置定义,应该是都不需要修改才对。这样,你的配置系统,才是与配置存储的方式是解耦的。

好,上面说了一堆要求了。方案呢?很简单。放配置项的值就三个地方,

  • Properties文件:开发负责。通过发版,或Config的Refresh Scope来变更。
  • 数据库:产品负责变更的配置,一般要有相配套的后台页面在运行时修改。
  • 环境变量。环境变量的注入方式运维团队自己决定就好。因为这个是他们负责的。

其它地方,什么Docker file啦,K8S配置啦,启动参数啦,原则上统统不放配置。
至于实现,用Spring自带的properties支持就可以达成效果,通过profile来区分环境。日后如果要用Vault,与Spring Cloud Config集成也简单。

应该谁来配置?

这其实就是上面一节中的一点。但是我感觉非常重要,所以想单独拿出来再说一遍。因为“谁来配置”,基本上决定了配置的方式甚至能进而影响服务的设计乃至架构。有人可能从字面上看到这个问题就反感。因为一般情况下,一个事儿,一旦聊到“谁”来做这个问题,大都就开始推诿扯皮了,因为很多人的心态就是“多一事儿不如少一事儿。”,而不是考虑这个事儿“应该”谁来做。这里的“应该谁来配置”的重点,恰恰就是“应该”是谁。而不是“谁”来做比较快,比较简单。

那么配置到底“应该”是谁来配置呢?一般而言,谁有权力决定这个值应该是什么,谁来配置。注意这里的“权力”是发言权、专家权,而不是职权。那么谁有专家权呢?

比如,API的错误率的告警阈值,传统的做法就是负责系统监控的运维人员来决定一个全公司统一的值。比如5%,高于这个就告警。但是每个API的情况不一样啊。比如12306的图片验证API,错误率正常情况下肯定不止5%,谁不得试了两三次的?你配置个统一的阈值必然没办法普遍适用。 (注意,不要抬杠说这里验证失败也应该是200,这里只是用这个接口举个例子)

于是有的公司改进了,专门做个页面,允许每个Team的负责人去配置每个API自己的阈值。于是又有新的问题了,比如:

  • 负责人说,我也不知道每个API正常情况下,错误率是多少啊?那个谁谁谁,你来拉下数据。
  • API改进了,用异步操作了,总是直接返回结果,然后错误率忘改了,然后一直没有任何告警。

Team Lead是专家吗?Team Lead其实也不可能事无巨细地知道一切,不可能实时跟进API的变更,也不可能不顾实际情况地强行配置一个值,那和一开始由运维配置一个全局的值没有本质区别。本质问题是什么呢?Team Lead其实也决定不了一个API的错误率。只有写这个API的人才能根据自己对API的了解做出判定。即开发本人,确定每一个API的合适的错误率告警阈值。对于非常专业的工作,实际做的那个人,才是专家,而且必须是专家,不然他是做不好的。这里的专家并不是方方面面的全才,而是指在自己负责做的这一个小部分,在整个公司范围内,他必须是专家,至少是之一。

当然,这么做是有两个假设,一是假设开发都是认真负责的,不会配置个99%然后就安心地去睡大觉了,其次Team Lead也要愿意相信Team Member的判断。另外,“合适”并不表示“符合预期要求”。比如开发人员确定了一个数据查询接口,由于bug多,其错误率本来就高,认为告警阈值配置到30%才真表示有问题。Team Lead的责权在于,决定让开发人员改进接口,把错误率降到3%,才允许上线。

重点来了,如果一个东西让开发去配置,那方式就多了。而且一般不用单独做个配置页面这么重。常见的有:

  • 配置文件。比如properties文件。
  • 数据库migration脚本。比如Flyway或是DbMigration来修改数据库表中的配置信息。
  • 甚至代码本身

这里说的在代码中做配置并不是hardcode在代码里,具体的例子可以参考bucket4jbucket4j-spring-boot-starter这两个项目中的示例。对于需要由开发来配置的参数,这种方式势必会是方案之一。

注意:这两个框架我并没有用过,而且是做限流,不是做告警阈值配置的。只是按自己解决问题的思路,找到的现成的和上面说的方式比较一致的框架,提一下给大家做个类比参考,我也省下自己去写example的时间。

最后再强调一点,我写这篇文章,并不是要鼓吹在“代码里做配置”这种方式。 而是在讲,做一件事的时候,最好先把思路打开,别把思路局限在一个小圈圈里找方案。“代码里做配置”,只是一个代表,代表着那些从一开始,由于一些成见、思维定势而被排除在可选项外的可选方案之一。

压力测试与服务容量规划

压力测试的目标主要有两个,一是验证服务技术指标是否符合要求,二是为服务的实际部署方案的规划提供基础数据,以便回答这个看上去很简单的问题:要什么配置的服务器?要几台才够用?然而这个问题其实并不那么简单,因为服务规划并不是简单的四则运算。因为其相关指标之间是有相互影响的,会需要做些权衡取舍。
为了达到以上两个目标,压力测试的一般步骤可以这样做:

  1. 确定服务能达到的最小响应时间。这个一般是在低压力情况下得出。
    • 同时,根据最低响应时间,我们可以大体计算出单个用户,单服务节点的最大QPS。
  2. 在高并发的情况下,测算出单个服务节点所能承载的最大QPS。
  3. 通过模拟真实请求的使用模式,测算出目标响应时间下的最大并发用户数。
  4. 验证上述最大并发用户数下,长时间运行的服务稳定性。同时系统负载小于60%(后面解释)

真实请求模式的模拟

这一点非常重要。因为测试时请求模式的不同,会导致压力测试的结果的差异非常大,尤其是系统响应时间。请求模式真实与否的主要考虑点有以下三个:

  • 请求发出的时间点,是否符合平均分布。而不是出现一个个的请求量尖峰。
  • 每个连接发出的请求间,是否有合理的间隔(也称为思考时间)。而不是全火力发请求。
  • 模拟过程中,系统负载也要控制在合理范围内

我们做这样的一个最理想情况下的思想实验。假设一个接口的响应时间是1ms,支持的并发量为1(即一次只能处理一个请求),用1000个请求来做压力测试。我们来讨论一下两种不同的请求发送模式:

  • 模式一:假想这1000个请求是每隔1ms发一个过来。那么每个请求发来的时候,上一下都刚好处理完,新请求都会毫无延迟地得到处理。从客户端看来,这1000个请求的响应时间都是1ms。
  • 模式二:假想这1000个请求是在一开始一起发到服务器端。第一个请求会被及时处理,之后每个请求的等待时间都比前一个多1ms,到第1000个请求,要等999ms才会被开始处理。从客户端看来,这1000个请求,虽然和模式一一样,也是在1秒内全部处理完成,但是平均响应时间是500.5ms。

可以看到,在上面的两种不同请求模式中,最终测试出来的响应时间相差500倍。当然,这个例子是理想情况下的两个极端。现实中的差别不会这么大,但是也非常可观。由于压力测试脚本产出的请求模式的不同,在结果上产生几十倍差距的情况也是屡见不鲜的。

系统负载与响应时间的关系

在上一个思想实验的模式一中,我们发送请求是均匀分布的,但是更真实的分布,是时间齐次的一维泊松分布(一种随机分布)。有什么不同呢?请求平均分布的情况下,我们可以想象,只要负载没达到100%,响应时间就应该一直是1ms。但是在真实的情况下,我们这个假想接口的响应时间会是系统负载的一个函数。关系函数如下(其中,s表示请求处理时间,p表示系统使用率,r表示响应时间。):

r=s(2-p)/2(1-p) 

函数图像如下:

响应时间随负载的变化

我们可以看到,负载在80%的时候,响应时间就会是三倍于处理时间,而负载在90%以上时,延迟就会直接彪高。

注意这个公式并不具有普遍适用性,只能用于等候理论中,符合M/D/1型队列的情况,而且是假设服务器本身的性能不会因为负载的变化而变化。而真实的情况,会比这个更加复杂,服务接口的处理时间,常常本身也是负载的一个函数。通过这个分析及图表想要说明的是,如果想要保证系统的延迟在某个范围内,就要确保服务本身的负载不能过高。

一开始说,压力测试的一个目标是验证服务的技术指标是否达标,但是技术指标有很多,不同指标的压力测试方式也是不同的。不可能用一次压力测试,把所有的指标都能验证完成。如果想测试系统延迟是否达标,就要在测试过程中保重负载不过高。要知道,实际生产环境中,一般负载大于60%就要开始着手扩容了。根本不允许负载过高的情况出现。压力测试,还可以验证,在负载小于某个点时,比如60%,能撑住多大的量。用100%去测试,但是生产又不跑到100%,测试的效果就要打折扣。100%负载的压力测试当然也有用,可以用来验证服务在高强负载的情况下,服务的可用性、稳定性是否依然符合要求,不能宕机,不能无响应,不出错,基本上就可以说稳定性就是好的,但是这个时候的系统延迟是不可能好看的。

对压力测试一个常见的误解就是:压力测试嘛,时时刻刻的负载都得是100%才叫压力测试。

集群测试的目的

到4为止,对单台服务器的测试基本完成。后面就可以开始集群测试了。集群测试的目的是,验证服务的水平可扩展性是否良好。水平可扩展性良好的服务,加一倍的容量,就能多承载一倍的负载。水平可扩展性不好的服务,多加多少节点,都没办法支撑更多的负载。造成水平扩展不良的的原因,一般都是服务内部有核心操作串行化。更常见的情况是:加一倍的容易,可以多承载X倍的负载(其中因子X小于1)。对集群进行压力测试的主要目标,就是测算出这个X因子的值。以用于服务容量规划。在实际的规划过程中,会有两个方向:

  1. 保目标响应时间。一般用于对响应时间要求高的系统。就直接用最佳并发用户数去做规划就好。最最理想的情况下,如果1台能支撑100用户,10台能支持最多1000用户。
  2. 保每台最大并发用户数,以节省服务器资源。如果可接受的响应时间比较高,那么每台服务器就可以承载更多的用户。这时需要再进行一次,以测算单台服务器的最大并发用户数为目标的压力测试。并以这个最大并发用户数去做容量规划。

所有服务容量规划的前提,都是服务本身是可以水平扩展的。

下面以K6压力测试的结果为例,简单演示一下以上过程。

示例测试环境

Mock的被测试服务接口如下所示。没有数据库访问。没有同步。这样做的主要目的是简化模型。现实情况的结果,会比下面的演示复杂得多。

@GetMapping("/say")
fun sayHello(@RequestParam greeting: String): Mono<Any> {
    return Mono.just(greeting)
}

运行环境:Docker 4 Core 4GB on Mac Book Pro with 2.8G i7 CPU 同环境还运行:Spring Cloud Gateway, Prometheus, Grafana, InfluxDb及K6 Test Engine。

最小响应时间的测量

测试方法:使用少量用户(比如一两个),无思考时间(全负荷)发请求。

期望结果:响应时间保持极低水平。QPS也要足够高。

如下图所示:

平均响应1ms,95%分位1.5ms。后文为便于计算,最低响应时间记为1ms。 下图中,QPS最高1500。但是可以想象,这并不是这台服务的最大QPS,毕竟用户太少了。 根据响应时间,我们大体可以知道,单个用户,在单个服务节点上,最大QPS是1000。

最大QPS的测量

测试方法:使用大量用户(使用10,30,100,300每别测算),无思考时间发请求。

期望结果:一开始增加并发用户量,可以显著提高QPS压力,但是用户增加到几十个(甚至几个)之后,QPS基本就上不去了。

如下图所示:

我们用100个高并发用户去压,并没有把QPS压到100*1000这个量级。我们发现最高也就到3200QPS,就上不去了。而且服务的响应延迟显著增加。这是非常正常的。因为无思考时间的用户,等同于在做DoS攻击。正常用户的正常API调用,是不可能出现这种全负载狂发请求的情况的。如果你说你的API特殊,就是每个用户要1秒调用1000次以上,那你其实最好重新思考下这个设计是否合理。

真实负载下的并发用户数的测算

上面我们已经讨论过什么是真实负载。于是在这个压力测试中,我们要引入思考时间这个概念。

测试方法:从0开始,逐渐增加用户数到200。加入50ms的思考时间(即每个用户,每秒最多发20个请求)

期望结果:观察响应时间的增长情况,用户量达到某个点后,响应时间开始快速增长。这个点,即是单台服务所能承载的并发用户数。当然,如果你对响应时间的要求不太高,也可以根据某个特定响应时间,比如200ms,来确定并发用户数。而不是非得用响应时间的增长拐点。

如下图所示:

用户在达到100左右开始,响应时间开始变高,QPS也不再随着VU线性增长。所以我们可以说对于这个服务节点来说,最好只用来支持100个用户。但是你也可以发现,即使到200并发用户,系统也还是OK的。只是延迟比较高。

这里的压力测试中使用的假设的思考时间的长短,是非常影响并发用户数测算的。如果我们把思考时间加到500ms,就可以支持更多的VU,因为QPS一般是固定的。下图展示了600VU在500ms思考时间下的测算结果。是不是看上去比上面100用户的时候还好?(注意,QPS都是1000左右。)

资源访问控制上的一些思考

业界有很多种权限控制的设计模型。做过访问控制的人或多或少都比较过这些方案的优劣。我过往的一些相关经历,让我发现一些在做这个选择的时候,可以思考的问题。简单梳理一下,以作备忘。

引入访问控制的目的是什么?

这是非常重要的问题。是“为了安全”吗?不,这个目的太宽泛了,宽泛到无法指导你做任何有效而且合理的决策。作为一个安全策略的制定者,除了“为了安全”之外,必须要搞清楚,你要防范的人是哪些,想要控制其的哪些行为。不用很细,但是至少是“原则上”地。比如:

  1. 要防范的人,分哪些?公司内部,公司外部两类吗?防范方案有什么不同?
  2. 防范他们的什么行为?是要防范他们无意的误操作?还是要防范他们恶意的删库跑路?真的都要吗?
  3. 你愿意为了“安全”将效率牺牲到什么程度?比如,为了保护代码安全,最安全的做法,显然是给电脑断外网,并且禁用除显示器外的所有输出设备,不允许带入手机,也不允许把电脑带出,而且办公室不能有窗户,以防对面办公楼的长枪短炮拍摄到你屏幕上的代码。这个时候,你可能牺牲的不仅仅是效率,可能还有员工的技术成长、身心健康,以及这个岗位的离职率。这可不是说段子,很多外包IT公司的部分项目就是这么搞的。

访问控制的主体是谁?

或者说,谁应当受到访问控制?

  1. 人。人分哪些?内部员工,外部用户,还有吗?他们的权限模型需要统一吗?能够统一吗?
  2. 服务。首先,服务本身需要访问控制吗?我认为是必要的。因为服务所应当有的权限,是很难和公司内部某一个人的权限完全一致的。而且,人的变动会远多于服务的变动。对服务本身的访问控制的管理成本,一般也会远低于对人的访问控制的管理成本。另外,不要觉得对服务这个东西的访问控制很不自然,你手机上的App,也算是一种服务。而你肯定是想管理App的权限,而不是App的开发者能在你的手机上干什么。我见过的最奇怪的规矩是,权限只能给人,不接受给服务本身设置访问控制方案,但是接受服务以root身份运行,也接受负责服务的人,拥有本当属于服务的权限,也即生产环境的所有权限。当然,我也理解他们给出的解释:出了事儿,他们可以很方便地定位到责任人。而且他们只需要维护人的权限,完全不需要考虑服务的权限这么个事儿。这对他们自己而言,这的确是最安全的方案。
  3. 要统一吗?人类才华产生的最大非理性的冲动,就是见到分开的东西,就想找到一个统一的方式去处理,比如爱因斯坦干的那事儿。也许能统一,但是作为一个企业,而非一个科学家的时候,你要着重考虑的是成本收益及可操作性。而不是一个方案看上去有多完美多NB。当然,如果你要拿着这个方案去骗某个风投的话另当别论。

访问控制的客体是谁?

或者说,控制对什么东西的访问?显然并不是所有的东西都需要访问控制的。但你判断一个东西要不要做访问控制的条件有那些?要不要花钱?会不会失控?举几个例子:

  1. 办公室空调的按钮。据我了解很多人都希望有,但是这并不需要权限系统来解决,如果真要暴力解决,这只需要一些胶带并让公司行政把遥控器收好。
  2. 数据库DDL。这个一般都是要控制的,但是再考虑下主体,你要想好想让哪个主体来做。是人,还是服务?是开发还是运维?如果你选择人,那基本上就和市面上的DB Migration框架说再见了。这也说明了,访问控制上的一些高层面的决定,会影响你服务内技术栈的选择。
  3. 服务运行时环境变量及参数。这个伪逻辑是这样的,环境变量的变化,可以彻底改变应用程序的行为,所以要严格控制对环境变量及参数的改动,只能由维护人员有权限改。于是上线的过程,就需要由开发给运维交待一整页配置变更。于是,基本告别Continous Delivery了。当然,这个时候人们一般都会安慰自己说,我们其实也并不需要CD,而不是反思前面的问题有没有更好的解决方案。毕竟项目都是有Deadline的。

谁负责设计资源访问的细节?

我之前在一家投行工作,公司使用的是一个内部自研的权限控制系统,混合了ABACRBAC两种方案的优势,其权限数据结构是可定制的,由各个业务团队自行定义资源的一切细节,非常强大灵活。那段时间,负责这个框架的那个团队,开始做这样的一个事儿:用AI技术,分析当前的权限配置中不合理的部分。因为公司当时面临这样的一个问题:连业务团队自己,都已经无法看出自己的权限数据中有什么问题了。因为绝大多数情况下,人们使用这个框架的方式都是:简单看下原来的数据是什么样的,用最少的改动,最少的数据量,就可以完成这次权限配置变更的要求。而不会去深入地思考,“应该”做什么的数据变更是最合理的。最后这个权限控制数据库就失控了。不过好在人们都知道这个问题,并且在积极解决。所以再好的框架,再好的设计,也会因为不当的使用而难以发挥其里大的价值。那么哪些是使用上的细节和设计上的细节呢?比如:

  1. 公司到底有哪些角色?这些角色应该有哪些权限?这些细节是技术人员先定一个,还是先和相关方讨论清楚?
  2. 公司有哪些资源是比较容易分辨的,但是每类资源应该有哪些属性呢?是加属性好?还是加新资源好?
  3. 可以为每笔交易都定义一个独立的资源项吗?
  4. 客服需要客户信息,交易员也需要客户信息,但是需要的部分不同。这时,这两类客户信息,是两种资源吗?

类似以上这些问题,无论如何选择,都是可以完成工作的。但是质量上却有云泥之别。只是有些差别,有些后果,要出现后续的状况时,才会体现出来,做决定的时候,常常因为“都可以”而无视掉。

谁负责设定资源访问的权限?

即授权。一个简单的回答是:owner。然后下面的问题就是,谁是owner?我只想表达一点,从权限控制的角度,同一个东西的不同方面,其owner可以是不同的。比如:开发写了一个接口,这个接口谁能调用谁不能,肯定不是这个开发人员决定的,至少也要构架师才能决定;但是同一个接口,假设为了保护自身稳定性,添加了访问频率的上限的设定,在不影响整体服务功能和可用性的情况下,这个设定的owner可以是而且应该是开发者本人。

说说“对事儿不对人”的打开方式

“对事儿不对人”大概算是少有的几个无论内资外资都会声明认同的行为准则之一。但是却少有写进公司官方员工手册进行详细解释的。我不是官方,但是我想解释一下,也许你还能顺便悟出这个词为啥没有官方的详细解释了。

开宗明义,对事儿不对人,更合理的做法,是指做决定、决策的时候对事儿不对人,而不是指提建议或批评的时候对事儿不对人。容我娓娓道来。

声明

体制内的看官看看笑笑就好。我这里说的是体制外的情况。

建议与批评时

我们回想一下,批评一个人,或是给一个人提建议的时候,最重要的一条是什么?是要私下进行。对不对?你公开批评一个人,哪怕你批评得对,哪怕你职位比他高,有的人也会不接受,因为面子挂不住啊。他面子挂不住了,搞不好还要牵扯出你祖上十八代来撑场子。所以说正常人根本不会在公开场合批评某个人,还有什么“对事儿还是对人”的问题呢?

那么,公开场合,审明公司组织纪律算什么?比如有人天天拿公司牛奶洗脸,公司开全员大会,你猜会怎么说呢?会说是谁干的吗?不会啊,会说牛奶洗脸这事儿吗?也不会啊,你如果说,不让牛奶洗脸,那洗手呢?不让用牛奶,我用咖啡粉行不行?场面上会这么讲,公司将严厉惩戒将公司资产私用的情况。这叫既不对事儿,也不对人。使用官方用语。泛指你们所有这些烂人干的所有这些破事儿。普通员工去批评人,哪怕是批评一个群体,也不敢用这种“既不对事儿,也不对人”的口气,因为规格不够。也就CXO才可以。所以公开批评也没有“对事儿不对人”的问题。

好,那我们说私下批评,想象一下,你找到了那个用牛奶洗脸的员工。用对事儿不对人的说法怎么说呢?

A: 小王啊,我听说公司的牛奶常常失窃,搞得大家天天只能喝黑咖啡。你离厨房近,你帮忙看着,发现了告诉我一下,我们看能不能把这个小王八蛋逮着。

W: 你拐弯抹角地骂谁小王八呢?

而且,其实公开场合提建议也应该针对具体人。 比如代码评审会的时候,

我提一点儿,不针对任何人(哦,原来是要说我们所有人)。按编码规范,方法参数最好不要超过5个。我看刚才不少代码的参数都十七八个了,这个会后改进统一改下。

完了,这事儿肯定没人干。有人感觉我没问题不用管,有人有问题,还没到十七八个,等别人改好了我再改吧。最多的那个人压力本来最大。一听到“统一”改就想着这事儿不用我个小愣头青操心了,肯定要大牛出手啊。 当然我这里YY,实际情况不会这么糟,但是显然也不会太好。

试问,提出问题又不针对人,这事儿还怎么SMART化?每次都是:“我不是在说某个人,我是说在座的所有人。”不成? 所以提建议,批评,就应该对人。否则起不到效果。

换一个角度,如果一个人连别人私下的中肯的批评都承受不起,我不知道这个人还能承受得起什么? 而且如果你批评得对,也不用担心被批评的人,用“对事儿不对人”反驳,因为听上去很二。比如:有人随地吐了口痰,你应该不大会听到这样的对话:

A: 唉,你怎么随地吐痰啊。

B: 唉,你这人批评不要针对人好不好。

倒是可能会听到这样的对话:

A: 唉,你怎么随地吐痰啊。

B: 你找打?

人与人之间意见不同是非常正常又常见的,有分歧就打开门讨论。拐弯抹角地旁敲侧击得搞到什么时候? 当然,对人,不表示针对某个人,更不表示人身、人格攻击式的批评与建议。

决定与判断时

最后来说说,什么叫做决定做判断的时候,对事儿不对人。

我举个例子, 线上一个服务负载过高,需要找运维人员加一个节点。我相信如下对话模式,做过开发的应该都不会陌生。

开发: 能给XXX服务加个节点吗?

运维: 你说加就加啊?自己服务有性能问题先解决自己性能问题啊。

VS:

总监:能给XXX服务加个节点吗?

运维:好的,马上,好了告诉你。

另一个例子:

开发:能不能从你们的ElasticSearch里直接读数据啊?

运维:这不行的,万一你们做个大查询什么,直接就把日志服务搞挂了啊。

VS

总监:能不能从你们的ElasticSearch里直接读数据啊?

运维:我想想啊,也行,注意控制下查询参数不要太宽哈。

这就属于对人不对事儿。一个客观的事儿,能不能做,要不要做,应该由客观情况决定,不应该看这个事儿是谁来问的。至于某天晚上聚餐去哪儿玩,如果领导有想法,当然要听领导的。因为这是主观的事儿,没有客观是非。

对于这个问题,我还得意和一些从阿里出来的人聊过,我以为阿里这种政委风气重的地方,简直就是培育看人办事儿风格的沃土。结果对方十分肯定的讲,同一个事儿,谁去问都是一样的,不会有不同的答案。我对阿里的印象因此还好了些。

反过来讲,就算做决定的时候对人又怎么了?问题多了。比如工作低效啊。本来可以交给某个小弟去处理的事儿,搞得什么事儿都得我自己来,而且小弟们也会挫败感十足,好像自己什么事儿都搞不定一样,虽然问题根本不在他们那儿。我要再有点儿心,还要去安抚小弟。我怎么说呢?这不是你的问题,这边儿风格就这样,适应适应吧。他心想,哦,公司这个问题好像有点儿严重啊,我还是换一家吧。

更宽泛地讲,对事儿不对人,本质上是在说,在工作上,要以事实为依据通过理性思维做决定,而不要感情用事。 回到一开始的问题,你猜为啥一般公司都不会详细解释“对事儿不对人”呢?

有人听到这儿就上头了,说实现中,你看看哪家公司是这么做的,完全不切实际!你说得对,就像几十年前会随地大小便那帮人,看着“不要随地大小便”的标语时,也是一脸的莫名惊诧:大家都随地大小便啊,这个标语完全没说到重点啊,明明应该写“随地大小便时请注意遮挡”,这才合多数人的心意。 是吧?

从模式匹配与类基多态之间的选择说说项目的良性成长

一个选择问题

网上可以找到不少讨论这个话题的文章(如果你不知道为啥这两个东西要放一起比较的话,建议你先读懂以下这两篇,或者跳到下面讨论重要及解决方案的部分):

本文不讨论模式匹配和多态是什么,这种知识性问题。本文只想先讨论一下,如何正确合理地使用这两样东西。

因为了解一个东西,其难点从来都不是知道它是什么,它有什么优缺点。这些都是非常简单的。真正的难点是,拿到现实场景中,分析出并现实中需要考量相关因素是什么,分清主次,并做出合理的选择,以及,最容易被忽视的:接受相应选择背后的成本。否则就会出现,知识性的东西说起来头头是道,但是就是用不对地方的问题。举几个反面教材来做热身。

场景一:

A: “我这个类有好多字段,写个equals和hashcode方法都要写半天,apache commons用起来也不方便。”

B: “用data class啊,自带了equals和hashcode的实现啊。少撸好多代码,多Clean Code啊。”

场景二:

A: “这边儿处理各种税费的case好多啊,处理起来好乱啊。”

B: “用case class啊,然后用模式匹配给各个情况分别处理,多清楚。”

你看,这一来一回的字面上的逻辑都是通的。但是其实逻辑狗屁不通。 或者至少是没用到点子上。

做正确的选择并承担成本

说了一堆废话,还是开门见山地陈述一下自己的观点。

多数文章都会讲到:“模式匹配,便于扩展功能;多态,便于扩展类型。”但是从来不会提到前提及代价是什么。所以我想补充一下。

关于模式匹配:

  • 用对的前提是,维护好功能实现的完整性。一个功能就用一个函数或函数组实现。什么意思?意思就是当一个新的类型加入的时候,直接修改现有代码分支,而不是在外面包一层,谓之不动老代码,安全,还冠名Proxy啦,装饰者啦来justify这个拙劣的决定。这也展示了一种常见的出品屎一样的代码的方式:一个代码库中,不同的开发者,把不同设计理念杂糅在一起,相互交织。每个人的想法单独拿出来或许都是对的,然而不那么上心地堆放一起,就变了一坨屎。
  • 另一个前提是,最好用在类型真的不大会新增的场景下。否则不是给自己找麻烦吗?这个说起来容易,到具体问题上就容易抓瞎,一年有四季,不会有五季,这种一看就适合用模式匹配。屋子里的房间类型,客厅厨房什么的,也还行。但是如果产品拍着胸口说句话,税费就这几个类型,然后你就开心地决定用模式匹配了?
  • 代价是,以整个函数为单位实现功能,功能就是封闭在函数内的,你没有办法在不修改现有代码的前提下,修改一个现有的功能。这算什么代价?代价要看场景,我举个例子,插件体系。基本都是通过新加类来扩展功能的。没有哪个插件是通过动态修改主体函数内实现来扩展的,因为不好改啊,有也是Hack,需要很底层的机制,成本也很高。所以没有哪个插件体系是基于模式匹配的。当然还有别的场景,引入模式匹配的代价也是完全不可承受的。要具体分析。

关于类基多态:

  • 前提是,不要对“便于类型扩展”有什么误会,如果是那种会产生类爆炸的场景,无论是模式匹配还是类基多态都是不合适的。你可能需要的是数据化,也可能更需要重新思考你的类体系设计。
  • “便于类型扩展”的代价是,同一个功能,其实现逻辑,会散落在各个类中。于是会相对比较难以从代码上,获得对某个功能的完整的全局的认识。但是这不应该是你声称一个代码不可读的借口。多态的代码读不懂,是你自身水平够的问题,不是写代码的人写得烂的问题。

我们选择一个东西好的一面的同时,事实上也同时必须承担相应的成本。选择不仅仅要看正面的效用,也要乐于接受其负面的损耗。最怕是什么呢?两个方式的好处想全要,又想易于对类型扩展,又想易于对功能扩展。讲道理鱼与熊掌不可兼得,他开启杠精模式,说那是你水平问题。也是,我的确没有水平创造出这样一门全新的语言来。

这种选择有多重要呢?

最后一个问题,这个选择,把这俩东西用对这么个事儿,有多重要呢?

从现状来看,一点不重要。我这工作十来年了,从来没见过哪个公司的Code Review(如果有的话)中,大家会聊到如此深入的设计理念上的细节。为啥?因为即便你用错了,如果就那么几个地方,加加班改改就是了。如果用错的地方太多,而且错误又不限于模式匹配用得不对这一个问题,就干脆立项重写好了。把产出垃圾代码的锅甩给前任,现任人员从上到下都是功绩。所以没人死扣代码质量。况且,也不是所有人都喜欢讨论这种问题。有的人喜欢死扣细节,有的人还不高兴:“没bug能work的代码,非他妈要讨论写成啥样好,还讲一堆大道理。早点回家吃饭不好吗?蛋疼。”。

然后呢,重写后的第一版,一般都是好的。但是之后,就也走向越写越烂的老路了。然后又是新一轮的重写。最终结果呢?每一轮产出的都是垃圾。如果公司业务良好,走在快车道上,不差钱,倒也不在意这些重写的损耗,只要每次打完补丁,每次重写完,都能满足业务要求就好。人不够就加上嘛。有人什么垃圾代码搞不定?但是如果公司处于一片红海,整体经济环境又不乐观呢?一个人写垃圾危害也许还可控,如果一个公司整体风格就这样,那这些拙劣的设计和代码,迟早会拖累公司的成长速度。

还有一个问题是,如果一个人,其工作涉及到了模式匹配,多态的使用,但是他都没有办法正确使用的话,你如何相信这个人,能正确地使用各种设计理念更加复杂的框架与中间件呢?比如Spring Boot, Spring Cloud, Kafka, RabbitMQ。我是不相信的。

还有一个问题是,如果一个公司,对这些完全不在意,不去鼓励甚至要求员工去关注细节,一些员工就会觉得没有成长(尽管满眼的机会他看不到),没有技术含量(尽管一堆的技术问题没有解决)。然后这些一般般的员工也就离职了。优秀的员工是什么态度呢?他们这种嗅觉敏锐的,会第一批走,才不会甘心在一滩烂泥里同流合污呢。剩下的,就是没有丝毫追求,没有技术水平的员工。毕竟他们找新工作一般也比前两种人困难。 当然这是里糟糕的结果,也比较少见。我干了这么多年也只是听说过些故事。大家当段子乐乐就好。

解决方案

写了这十多年代码,对于如何解决这些问题,小有一些心得。如果一个团队的人,可以做到如下几点,是可以遏制项目腐烂的趋势,如果都做得不错,项目才能长期地向上成长。(如果这个公司的战略需要这个项目长期存在的话)

  • 不要Patch它,Fix它。我是这么判断是不是打Patch的。当你去改代码的时候,脑子里萦绕着“尽量不要动老代码,仅仅针对发生问题的这个case就好”的时候,你就是在打Patch。在以快速和不影响现有业务的幌子,行无脑不负责任编码之实。因为真正Fix一个东西,是要从这个问题是如何产生的开始,到仔细考量最合理的解决位置,以符合现有代码的设计思路的方式来处理。这个工作量与Patch不可同日而语。但是一个维护良好的代码库,Fix一个问题的成本永远是可控的。只有失控的垃圾代码,才会陷入“要从根本上解决这个问题,差不多要重写。”的窘境。
  • 认真地读现有代码,读懂。这是找到Fix方案的前提。如果你代码都没看懂,那碰到NPE的时候,那的确是,除了加句if not null,打个自己都不知道行不行的Patch,你的确也做不了别的更多的了。如果你感觉现在这个代码太垃圾,那太好,你展示才能的时候到了不是吗? 一般烂代码都是好读的,因为写的时候就没用脑子,只是分支多些罢了。设计精巧的代码才不好读,因为写的时候就是花了脑子的,你也不用指望自己脑子都不过就能轻而易举地看懂。如果你连烂代码都读不懂,那我建议你转行吧。读代码是基本功。
  • 编写有效的测试。如果你连有效的,能发现代码变更产生的非预期的bug的测试都没有。你当然也是不敢动现有代码的。如果没有测试怎么办?太好了,你可以通过写有效的测试来尽快熟悉这个系统的功能。另外,光看代码覆盖率是没有用的,我可以轻而易举地写出100%覆盖,但是实质上毫无用处的测试代码。
  • 持续改进。这个涵盖比较广,细说能独立开一篇文章。但是这部分才是解决根本问题的精髓。不断地发现问题,解决问题,一个系统才能成长。把问题隐藏掉或是绕过,都不是长期的处理策略。这里的重点在于追求卓越的心态。比如,抱着“大家都是这么搞的,所以我这么搞就可以了”的心态,是没办法改进的。要持续改进还包含要实事求是。事实是什么样就是什么样,不看人,不双标。不要因为这个代码是CTO亲手写的就不敢动,不要因为一个需求是CEO提的就不敢讨论。自己到底在什么位置上都看不清,对与错都不敢坚持,是没办法改进的。

但是问题是,我还没有见到过哪个团队的所有人,都能做到如上所有点的。毕竟,每个人写代码的动机,都是不一样的呀。

聊聊一个自行车棚的故事

之前因缘既会看到一个单词,bikeshedding,和它背后的故事。想和大家分享一下。wikipedia上可以找到故事原文,我这里简单用现代文描述一下。

上世纪60年代,某国财政部讨论建设核电站的议案,该议案有三个部分。一个是预算10,000,000英镑的核电站,一个是预算350英镑的员工配套自行车棚。一个是每人每年21英镑的午后茶点供应。

在议案的讨论过程中,预算10,000,000英镑的核电站主体部分,经过2分钟的讨论即获通过。因为内容细节太专业了没人看得懂。 而且毕竟人们都认为这个事儿反正要搞嘛。

然后大家对于350英镑的自行车棚的预算金额的合理里展开了激烈了讨论。有人表示车棚用的铝质材料过于奢侈,应该用石锦以节省成本;还有人表示3个月的工期长得不可理喻,说自己在家里后院搭过一下,只用了3天时间。本部分最终以削减50英镑预算而获通过。

最后一个议题也花了一个多小时的时间,但是由于部分细节的缺失,会上没有对此部分展开表决。并要求会后继续补充细节。

这个故事当然是杜撰的,但是道理不爽。在现实中,如果我们细心回想品味,其实也不难找到对应的案例。 在我看来,在软件工程领域,类似自行车棚的东西也很多。比如传输格式用SOAP还是JSON。

与其在这种层面的技术选型上花时间,甚至花时间比较各个方案的代码量、可扩展性、性能,倒不如花时间讨论一下,如何更好地设计框架,以便我们能随时在这些选择上做出变化。

简单而言,对于技术公司,技术层面上对方方面面的斤斤计较也许是必要的。对于非技术型公司,技术层面的选择问题当然也重要,至少不能比同行用的差太多,但是更重要的是技术层面的灵活性,能够快速应对市场变化才是一个业务驱动型公司赖以生存的根本。我相信,所有的产品,都不会听到技术人员如是描述他们系统的现状:

  • “数据大多了要分库分表?大概至少两个月吧,涉及的代码太多,而且每个项目访问数据库的方式都不一样,整个过程风险也比较高。”
  • “加一个商品品类?这个牵扯甚广啊,需要多个部门协调啊。要不你兼职一下项目经理?”

在非技术公司,还有比技术层面灵活性更重要的事情,就是分析清楚,从业务和系统功能角度,我们到底需要什么。给这些业务功能的重要性分级,并把技术上的问题挂靠在需要这个技术点才能支持的业务功能上。以免空谈技术方案本身的优劣。(这里补充一下,“搞清楚我们到底需要什么”不仅仅是产品的工作,需要产品和技术协同才能达到。这部分内容超过本文范围就不展开了。)

这个话说起来容易,但是具体到实际问题,如果没有点儿功力,可能会面临这样一个严峻的问题:摆在面前的这两个东西,哪个是核电站?哪个是自行车棚?比如:

为了支持大量的业务消息数据,所以要引入Kafka做消息中间件,还要招聘相关的Kafka运维支持人员,为了实现对数据的保密,所以要对Kakfa消息加密,因为我们数据敏感,所以要有审记,要有ACL。 于是要和公司SSO集成。相关配置要能适应业务随时变更的要求,所以以上Kafka的定制行为,要能通过访问公司的配置中心中的配置,完成变更。

请问,上面这段高层需求陈述,从哪句开始陷入了bikeshedding呢?

当然并不是这种选择问题都是bikeshedding,比如用Java还是.NET,服务网关是用Spring Gateway还是nginx这种涉及整个技术栈及人员配置的选择问题还是需要慎重对待的。

但是那个问题又来了,一个公司的技术栈选择和人员配置,到底是一个公司的反应堆的一部分还是反应堆边儿上的自行车棚的一部分呢?