免费知识哪里来——Arxiv使用指南

如果你非常确定自己想要找什么​,​比如知道论文的名字(算法的名字)或者作者的名字,直接去Google Scholar上搜索是最快的。然而如果你并不是很确定自己想要什么,只是想要看看某个领域的最新发展,知道大家都在干什么,然而​却发现​Google Scholar给你的结果多半不靠谱,请读下去。

如果你非常确定自己想要找什么​,​比如知道论文的名字(算法的名字)或者作者的名字,直接去Google Scholar上搜索是最快的。然而如果你并不是很确定自己想要什么,只是想要看看某个领域的最新发展,知道大家都在干什么,然而​却发现​Google Scholar给你的结果多半不靠谱,请读下去。

引子​

​ ​最近半年,在我身上时常发生下面这类对话: ​

“Hey, 你知道XXXXXXXXXXXXX问题怎么解决吗?”

“哦,我刚好读过两篇相关的论文,等下发给你。”

……

尴尬的沉默,一般来说拿到文章之后对方就再也不会来找我了,虽然我是真心的想要交流,然而有一些好学的孩子会接着问:

“你的论文是哪里找到的呢?”

鉴于论文背景不同,我会丢一个链接过去——这个链接(网站)在大部分数学/物理/计算机/统计领域里面,都是https://arxiv.org/

Arxiv是什么

arxiv设计的初衷是一群物理学家们想要交流自己将要发表的论文,可以想象一下,在上世纪九十年代,人们用的还是软盘(包括5.25吋软盘和3.5寸什么的,存储空间可以说以KB计算),邮箱也不例外,投稿高峰期分分钟几百个KB的邮箱就会被文章塞满。Paul Ginsparg一看这不行,论文如果想要很好的被分享并审核,大约是需要集中存储的,就在1991年于LANL(http://www.lanl.gov/ 洛斯阿拉莫斯国家实验室)建立了arxiv的雏形,如下图所示。

当时有个萌萌的域名:http://xxx.lanl.gov/ ,到现在也可以被访问。不过由于LANL作为一个严谨的科学实验室实在是懒得运营​这个网站​,后来就被康奈尔大学接管支持。

然而,今天当我们谈到arxiv,就不能不说Open Access【开放获取】。 我们都知道,以前看文章是要钱的,直到今天,看大部分的文章也是要钱的。如下图所示,看一篇Nature​一般​要20美元,折合100多人民币。

​这个价格吧,​说贵不贵说便宜不便宜,在大部分有钱公司和牛逼学校买个site license​或者报个销​都是不成问题的,但是对那些有科研兴趣的个人或者是那些落后地区的学校,就很成问题了。像马拉维啊中非啊这种国家,到2016年,官方数据中人均GDP才400美刀(大约20篇文章),你让人家怎么玩嘛。​难道要让知识像财富一样集中在少数人手里么?

就好像我们一直希望互联网拥有中立性​(​电信运营商、有线电视公司等互联网服务提供商应当平等地对待所有通过他们网络的流量,网络服务提供商有区别地对待不同的流量可能会使大公司能够限制消费者的自由)一样,我们并不希望因为钱而阻挡了知识的传播。 ​​ 所以,我们有了布达佩斯宣言:

There are many degrees and kinds of wider and easier access to this literature. By ‘open access’ to this literature, we mean its free availability on the public internet, permitting any users to read, download, copy, distribute, print, search, or link to the full texts of these articles, crawl them for indexing, pass them as data to software, or use them for any other lawful purpose, without financial, legal, or technical barriers other than those inseparable from gaining access to the internet itself. The only constraint on reproduction and distribution, and the only role for copyright in this domain, should be to give authors control over the integrity of their work and the right to be properly acknowledged and cited.

作为开放获取的先驱者,我们要为arxiv——这一开放获取的先驱​鼓掌!为我们带来的好处就是,免费!免费!免费!

基于我的个人访问经验,大家如果想知道都有哪些杂志或者期刊提供免费的资源,可以参考这个列表:https://en.wikipedia.org/wiki/List_of_open_access_journals 图中是“数学”类目下提供开放获取的期刊。

实在找不到免费文章咋办?

我一般会直接发邮件问作者要,顺便聊聊自己的研究领域以及跟这篇文章相关的几个最新问题,成功率还是蛮高的,还可能交到好朋友。

说到Arxiv还有一样不能不说的就是LaTex——个人认为最美的文本编辑器(或者语言?),只是从学术圈儿跳出来之后发现大家好像都懒得用,在此由于篇幅所限,不再赘述。

Arxiv有什么

由于各种历史原因,Arxiv中文献的主要研究领域还是数理哲学,包括数学/物理/计算机/统计/天文/定量生物/定量金融等领域。截止到2016年的发表量统计可以参考下面这张图。

左图中是每年新提交的文章数目,右图是每年的发表量所占百分比(总和为1), “hep-“代表高能物理学,(hep-th+hep-ph+hep-lat+hep-ex), “cond-mat” 代表凝聚态物理学, “astro-ph” = 天体物理学, “math” 代表数学, “other physics” 表示物理学的其他领域( physics+nucl+gr-qc+quant-ph+nlin) “biology” 指的是量化生物学,“finance” 指的是量化金融,“cs”指的是计算机科学。

我们可以看到,在2002年之前,计算机科学领域的占比几乎可以忽略不计,然而到2016年已然占据了近五分之一,并且还在以极快的速度增长。而1992至1996年间占据了大半江山的高能物理学,到了今天地盘几乎被蚕食殆尽,只剩约10%的份额苟延残喘。沧海桑田,可见一斑。

​还有论文发表总量如下图: ​

​(信息来自:https://arxiv.org/help/stats/2016_by_area/index ) ​

从图中,我们可以清楚的发现三个事情:

  1. 所有论文发表量都在时间轴上爆发式的增长,在上面这张历史发表总量图中体现的尤为明显;
  2. 数学一家独大,不管是在年度发表量还是历史发表总量上;
  3. 计算机(cs)在历史发表总量中只占了8.3%,然而2016年的提交量占了18.3%,结合提交数量图的分析,真是长势喜人、值得期待。
  4. 每个月近10000篇文章提交(正式接受的会少一些,如果精准到感兴趣的垂直领域会更少)。

那么,不想只关心数理领域的咋办?

很简单,就好像任何市场一样,arxiv火了之后大批人跟风,所以我们现在有了生物学版本arxiv https://www.biorxiv.org/,心理学版本arxiv https://psyarxiv.com/ ,等等等等。当然,知识的沉淀往往需要时间,这些垂直领域的“arxiv”们到目前为止还不是非常成熟,所以我仍然建议大家结合上文提到的Open Access列表里的机构,去寻找想要的免费资源。

Arxiv怎么用

就像我在本文开头提到的那样,这个引擎最大的效果是当你并不确定自己想要什么的时候,去看看领域的最新发展,知道大家都在干什么。它最大的好处是值得信任。 ​当然,​同样值得信任的还有很多​​——虽然它们大部分都要钱,针对大多数​期刊​们​,可以按照影响因子从大往小排个序(​众所周知,影响因子这个评定标准就像高考成绩一样非常片面​,​然而这也是目前最普遍的解决方案了),前文中提到的Nature系列​和Science系列​,都非常值得信任。不做赘言。

很高兴的一点是,收费(还收的比较贵的)基本都是期刊,然而与其他领域尤其是生物学方向不​太一样​,计算机方向最顶级的往往是会议而不是期刊,而​会议往往是不收费的!这里举个例子,International Conference on Machine Learning 【ICML】,机器学习方面的顶级会议之一,其中所有的文章都可以在下面的链接里找到。https://icml.cc/Conferences/2017/Schedule?type=Poster

太幸福了!​——这个也是我最最最推荐大家找到值得读的文章的办法:盯着领域著名会议列表刷(虽然有个缺点,就是刷到啥算啥)。

但是,往往新入门的人往往没有办法靠关键字搜索【看脸】来分辨哪个期刊哪个会议才是真正有价值的。每年的会议不知凡几,我在wikiCFP上随便搜了一下人工智能,在接下来一年内仅仅这个垂直领域就有3130个会议。平心而论,有多少是真正有价值的呢?每个会议按照50篇paper来算,15万篇paper,​一个人类​就是不吃不睡​不工作全职看一年​paper,能看几篇?

当然,据个人经验,关于计算机方向的会议排名,大家可以参考下面的链接 https://www.aminer.cn/ranks/conf ,我截取了AI/PR (人工智能/模式识别)领域排名靠前的一段,如下图。有想看计算机视觉的可以参考里面带”vision”的,具体会议的分析和比较将另文分析,这里不做赘言。

但是,如果不想这样盯着列表一个一个会议慢慢看下来呢?如果就是想要找一下某个主题或者关键字呢?或者就是想要知道这个月出现了什么新算法呢?——会议可都是一年一次的。 ​​ 这个时候,就是arxiv大放异彩的时候了。它给了我们一个更集中的搜索平台,而且相对来说更​值得信任(反正比​起来​什么都可以搜索的Google Scholar,用户要筛选的噪声少了很多)​。

没有证据都是耍流氓?​ 请点开下面这个链接 https://arxiv.org/list/cs.LG/recent,这是对Machine Learning领域的一个搜索。仅仅看这一页简介那些熟悉的作者​名字,就知道大部分都不会让你失望的。

​尽管arxiv定位是preprint,但​其中​也​囊括了各种已经被NIPS,AAAI等等行业顶级会议接受的文章​​。而且,所有这些,都有直接的pdf原文可以免费下载。搜索和筛选成本都低到极致。同时,只需要点击感兴趣的作者名字,所有他发表过的文章都会被列出来(如下图中的Lei Shu),而完全不用担心其他搜索引擎中常常发生的同名同姓作者的麻烦——这一点相信搜索过中文名拼音的都深有体会——张王李赵遍天下绝对不是说说的。

得数据库如此,夫复何求?

作为对比,下面是我在Google Scholar同样搜索Machine Learning关键字得到的结果。大家可以自行体会时效性,相关度和质量。

当然​,​时效性这个可以通过点击左手边的“Sort by date”来解决,然而点击了之后是这个样子的:

——反正前面Springer家的都是要钱的,质量不提,单单是能下载的pdf也真的不常有。​

​更重要的是,​google作为跨平台老牌全网搜索引擎,看到某个关键字的时候并不会针对特殊的领域进行搜索,所以大批量主题无关的文章(不信的可以搜一下lenet,vgg这类,看看搜索结果)​,就会干扰搜索结果。

所以,当你要​找的参考文献​属于数理领域尤其是AI/ML/Stat相关​,而​Google Scholar又没能给你满意的结果(又或者很贵),​就试试看arxiv吧!

 


更多精彩洞见,请关注微信公众号:思特沃克

Share

时间的回报

讲技术债也许太技术了,另一个很通俗的例子是破窗理论。讲如果一栋大楼的一个窗户破了不及时修复,会引起边上的窗户加速破损。我最近就遇到一个类似的例子,一个公共场所边上的角落有一些垃圾,过了一会有人拿着垃圾四处找了一下没找到垃圾桶,就把新的垃圾丢带了那个角落。如果这个角落一直没人清理,可以想象不需要一个星期就变成垃圾角了,甚至有人会大老远跑过来把垃圾往这个角落扔。

我觉得不理解或忽视技术债的人不是因为不懂技术,而是因为不敬畏时间的代价。

回报加速定律

在2017年咨询师会议上,我们提出了“智能原生”的口号,自然会去想人工智能未来会怎么样。其中一个令人恐慌的预测是“奇点”,奇点的意思是有一天电脑会比人聪明一点点,然后会更加极速的发展到比人聪明很多,直到完全控制人类。奇点预测背后的原理是Ray Kurzweil在2011年提出的回报加速定律(The Law of Accelerating Returns)。

回报加速定律通过分析人类文明的发展历程,发现人类发展不是线性,而是指数发展。原理也很简单,即发展的回报会加速发展。

最有名的例子是摩尔定律,芯片运算能力每18个月翻一番。10年前iPhone刚刚发布,摄像头只有200万像素, 现在新一代的旗舰手机一般都会搭载2000万像素的摄像头。10年前最先进大型机的图像识别技术可以分辨出兔子和茶壶,而且准确率还不怎么高,2017年新一代的iPhone X已经能离线进行脸部识别。

2017年的电脑智能水平大概比昆虫要高一点了,因为有了先进的电脑等设备(生产力提高),使得生产更先进的设备加速(生产效率提高),预测2022年比老鼠聪明,2030年比一个人聪明,而大概到2050年将比所有人类都聪明。

​(图片来自:http://t.cn/RQSRzFR

加速回报定律可以说是系统正向反馈循环的一个洞见。比如马太效应,在《圣经·新约》的“马太福音”第二十五章中有这么说道:“凡有的,还要加给他,叫他多余;没有的,连他所有的,也要夺过来。”。通俗讲就是有钱的更有钱,没钱的继续没钱。还有比如滚雪球也是同样的意思,亚马逊所信奉的飞轮理论也是差不多的意思。

系统反馈循环有正向,也有负向反馈。

技术债与破窗理论

我们在给客户做敏捷转型技术咨询的时候总是会遇到技术债的问题,即系统中的烂代码和强耦合。

技术债,这个词是个很伟大的发明,只是很少有人能理解其中的含义。只是把它当做烂代码的另一个代名词。

技术债的产生源自人的能力不足或者因为资源有限赶进度而牺牲代码质量,技术债并非完全不好。这跟公司或者个人负债是一样的,可以通过承担负债获取短期收益。然而不幸的是,因为低估甚至不理解技术债所产生的代价,人们往往会选择即使在有资源的情况下仍然忽视技术债、并且不偿还。

烂代码之所以叫技术债,是因为会随着时间会产生额外的代价。今天写一段烂代码不及时修复会引出更多的烂代码,因为后来写代码的人会仿造之前的代码,或者为了绕过烂代码而产生额外的烂代码,使得整个系统陷入烂代码泥浆。我们客户的很多系统寿命不超过3年,不是因为业务变化,而是系统实在太烂无法维护了,只能重新做一个。

讲技术债也许太技术了,另一个很通俗的例子是破窗理论。讲如果一栋大楼的一个窗户破了不及时修复,会引起边上的窗户加速破损。我最近就遇到一个类似的例子,一个公共场所边上的角落有一些垃圾,过了一会有人拿着垃圾四处找了一下没找到垃圾桶,就把新的垃圾丢带了那个角落。如果这个角落一直没人清理,可以想象不需要一个星期就变成垃圾角了,甚至有人会大老远跑过来把垃圾往这个角落扔。

我觉得不理解或忽视技术债的人不是因为不懂技术,而是因为不敬畏时间的代价。

(图片来自:http://t.cn/RQS8QVp

关于系统反馈还有一个很热门的话题,也许跟时间只是恰好有关,即天才一万小时定律。

一万小时天才定律

一万小时定律解释天才之所以卓越非凡,并非天资超人一等,而是付出了持续不断的努力;只要经过1万小时的锤炼,任何人都能从平凡变成超凡;要成为某个领域的专家,需要10000小时,按比例计算就是:如果每天工作八个小时,一周工作五天,那么成为一个领域的专家至少需要五年。

这个同样基于统计的定律非常有意思,我觉得也很有用。人们常常会低估或者忘记回报加速定律、技术债/破窗理论中时间的作用,对于一万小时天才定律往往只关注时间。世界上有太多人在同一个领域日复一日年复一年工作五年、十年、数十年,然而专家还是只是少数人。原因是什么呢——缺乏正向反馈回路。

收尾

前面讲了三个跟时间相关的系统回路定律,时间的回报有可能是正向也可能负向,更可能会加速。


更多精彩文章,请关注微信公众号:软件乌托邦

Share

phantomJs之殇,chrome-headless之生

随着Google在Chrome 59版本放出了headless模式,Ariya Hidayat决定放弃对Phantom.js的维护,这也标示着Phantom.js 统治fully functional headless browser的时代将被chrome-headless代替。

技术雷达快讯:自2017年中以来,Chrome用户可以选择以headless模式运行浏览器。此功能非常适合运行前端浏览器测试,而无需在屏幕上显示操作过程。在此之前,这主要是PhantomJS的领地,但Headless Chrome正在迅速取代这个由JavaScript驱动的WebKit方法。Headless Chrome浏览器的测试运行速度要快得多,而且行为上更像一个真正的浏览器,虽然我们的团队发现它比PhantomJS使用更多的内存。有了这些优势,用于前端测试的Headless Chrome很可能成为事实上的标准。

随着Google在Chrome 59版本放出了headless模式,Ariya Hidayat决定放弃对Phantom.js的维护,这也标示着Phantom.js 统治fully functional headless browser的时代将被chrome-headless代替。

Headless Browser

也许很多人对无头浏览器还是很陌生,我们先来看看维基百科的解释:

A headless browser is a web browser without a graphical user interface.

Headless browsers provide automated control of a web page in an environment similar to popular web browsers, but are executed via a command-line interface or using network communication.

对,就是没有页面的浏览器。多用于测试web、截图、图像对比、测试前端代码、爬虫(虽然很慢)、监控网站性能等。

为什么要使用headless测试?

headless broswer可以给测试带来显著好处:

  1. 对于UI自动化测试,少了真实浏览器加载css,js以及渲染页面的工作。无头测试要比真实浏览器快的多。
  2. 可以在无界面的服务器或CI上运行测试,减少了外界的干扰,使自动化测试更稳定。
  3. 在一台机器上可以模拟运行多个无头浏览器,方便进行并发测试。

headless browser有什么缺陷?

以phantomjs为例

  1. 虽然Phantom.js 是fully functional headless browser,但是它和真正的浏览器还是有很大的差别,并不能完全模拟真实的用户操作。很多时候,我们在Phantom.js发现一些问题,但是调试了半天发现是Phantom.js自己的问题。
  2. 将近2k的issue,仍然需要人去修复。
  3. Javascript天生单线程的弱点,需要用异步方式来模拟多线程,随之而来的callback地狱,对于新手而言非常痛苦,不过随着es6的广泛应用,我们可以用promise来解决多重嵌套回调函数的问题。
  4. 虽然webdriver支持htmlunit与phantomjs,但由于没有任何界面,当我们需要进行调试或复现问题时,就非常麻烦。

那么Headless Chrome与上面提到fully functional headless browser又有什么不同呢?

什么是Headless Chrome?

Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的前提下,使用所有Chrome支持的特性,在命令行中运行你的脚本。相比于其他浏览器,Headless Chrome 能够更加便捷的运行web自动化测试、编写爬虫、截取图等功能。

有的人肯定会问:看起来它的作用和phantomjs没什么具体的差别?

对,是的,Headless Chrome 发布就是来代替phantomjs。

我们凭什么换用Headless Chrome?

  1. 我爸是Google,那么就意味不会出现phantomjs近2k问题没人维护的尴尬局面。 比phantomjs有更快更好的性能。
  2. 有人已经做过实验,同一任务,Headless Chrome要比现phantomjs更加快速的完成任务,且占用内存更少。https://hackernoon.com/benchmark-headless-chrome-vs-phantomjs-e7f44c6956c
  3. chrome对ECMAScript 2017 (ES8)支持,同样headless随着chrome更新,意味着我们也可以使用最新的js语法来编写的脚本,例如async,await等。
  4. 完全真实的浏览器操作,chrome headless支持所有chrome特性。
  5. 更加便利的调试,我们只需要在命令行中加入–remote-debugging-port=9222,再打开浏览器输入localhost:9222(ip为实际运行命令的ip地址)就能进入调试界面。

能带给QA以及项目什么好处?

前端测试改进

以目前的项目来说,之前的前端单元测试以及组件测试是用karma在phantomjs运行的,非常不稳定,在远端CI上运行时经常会莫名其妙的挂掉,也找不出来具体的原因,自从Headless Chrome推出后,我们将phantomjs切换成Headless Chrome,再也没有出现过异常情况,切换也非常简单,只需要把karma.conf.js文件中的配置改下就OK了。如下

customLaunchers: { myChrome: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=9222'] } },

browsers: ['myChrome'],

UI功能测试改进

原因一,Chrome-headless能够完全像真实浏览器一样完成用户所有操作,再也不用担心跑测试时,浏览器受到干扰,造成测试失败

原因二,之前如果我们像要在CI上运行UI自动化测试,非常麻烦。必须使用Xvfb帮助才能在无界面的Linux上 运行UI自动化测试。(Xvfb是一个实现了X11显示服务协议的显示服务器。 不同于其他显示服务器,Xvfb在内存中执行所有的图形操作,不需要借助任何显示设备。)现在也只需要在webdriver启动时,设置一下chrome option即可,以capybara为例:

Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome,
                               desired_capabilities: {
                                   "chromeOptions" => {
                                       "args" => [ "--incognito",
                                                   "--allow-running-insecure-content",
                                                   "--headless",
                                                   "--disable-gpu"
                                   ]}
                               })
end

无缝切换,只需更改下配置,就可以提高运行速度与稳定性,何乐而不为。

Google终极大招

Google 最近放出了终极大招——Puppeteer(Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.)

类似于webdriver的高级别的api,去帮助我们通过DevTools协议控制无界面Chrome。

在puppteteer之前,我们要控制chrome headless需要使用chrome-remote-interface来实现,但是它比 Puppeteer API 更接近低层次实现,无论是阅读还是编写都要比puppteteer更复杂。也没有具体的dom操作,尤其是我们要模拟一下click事件,input事件等,就显得力不从心了。

我们用同样2段代码来对比一下2个库的区别。

首先来看看 chrome-remote-interface

const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
const fs = require('fs');
function launchChrome(headless=true) {
return chromeLauncher.launch({
// port: 9222, // Uncomment to force a specific port of your choice.
 chromeFlags: [
'--window-size=412,732',
'--disable-gpu',
   headless ? '--headless' : ''
]
});
}
(async function() {
  const chrome = await launchChrome();
  const protocol = await CDP({port: chrome.port});
  const {Page, Runtime} = protocol;
  await Promise.all([Page.enable(), Runtime.enable()]);
  Page.navigate({url: 'https://www.github.com/'});
  await Page.loadEventFired(
      console.log("start")
  );
  const {data} = await Page.captureScreenshot();
  fs.writeFileSync('example.png', Buffer.from(data, 'base64'));
  // Wait for window.onload before doing stuff.  
   protocol.close();
   chrome.kill(); // Kill Chrome.

再来看看 puppeteer

const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.github.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();

对,就是这么简短明了,更接近自然语言。没有callback,几行代码就能搞定我们所需的一切。

总结

目前Headless Chrome仍然存在一些问题,还需要不断完善,我们应该拥抱变化,适应它,让它给我们的工作带来更多帮助。


更多精彩洞见,请关注微信公众号:思特沃克

Share

请停止结对编程

这是一个风和日丽的星期五下午,Ben和Martin本应该在Costa咖啡馆喝一杯下午茶,一起聊聊周末的计划,然而PM的一个微信通知打乱了这一切。原来产品出现了一个bug需要紧急修复,下班之前必须要搞定。两人收到消息疾步走回到岗位,也没了心情去喝刚泡好的咖啡,连忙打开邮箱查看问题报告……

(根据真实事件改编,情节有所夸张,请勿对号入座。)

这是一个风和日丽的星期五下午,Ben和Martin本应该在Costa咖啡馆喝一杯下午茶,一起聊聊周末的计划,然而PM的一个微信通知打乱了这一切。原来产品出现了一个bug需要紧急修复,下班之前必须要搞定。两人收到消息疾步走回到岗位,也没了心情去喝刚泡好的咖啡,连忙打开邮箱查看问题报告。

开始

Ben:看来这不是一个很大的问题,就是处理一个来自于远端服务的异常。现在的情况是BFF(backend for frontends)在内部的远端服务有异常,会将异常直接返回到客户端,这样只要一个保单出了问题,前端所有的保单也都没法用了。

Martin:那怎么解决?

Ben:感觉可以在异常的地方加一个异常处理。这个涉及到RxJava和Java8的stream特性,我不是太熟悉,要不我们一起Pair吧

Martin:好。

两人喝了一口炙热的咖啡,摆好键盘鼠标,打开了IntelliJ工程。几分钟后,这个故障重现了。

Martin:可以重现的故障通常比较好解决。我们在这里先弄个try…catch试试。 两人似乎很有信心,然而重启项目后,故障并没有按照预期停下来。

Ben:hmm,这里为什么停不下来呢?

Martin:可能是RxJava的延迟处理,没有正确的捕捉到。这样,你在这里再写一个逻辑,然后在这里设个断点……

焦急

在这个过程中,Martin只是对着屏幕指指点点,时不时看看手机、在微信上聊聊天。Ben对RxJava并不是很熟悉,他想紧紧跟随Martin的思路,然而增加多个逻辑以后,依然都不能解决问题。15分钟已经过去,Ben这时候心生怀疑,是不是哪些地方没弄对?

Ben:我们理一下思路看看?

Martin:恩,来吧,一起看一下代码。

Martin领着Ben一起看了一下代码,并且一直在旁边指点Ben进行单步调试。由于RxJava的延迟特性,使得断点很难设置。而抛出异常的调用栈会出现在某些莫名其妙的地方,这让他们根本不知道把try…catch放在哪里才能奏效。

Ben:可能是要这样,在这里加一个OnError看能不能解决。

看似问题能够解决,其实是又一次的失败。在两人的激烈讨论中,时间过得很快,一晃眼已经是1个小时以后,咖啡早已经凉了,然而两个人完全没有心情,甚至都忘了咖啡的存在。

Ben对Martin的解决方案越来越没有信心,两人开始重新讨论起解决方案。然而方案是越讨论越复杂,看起来想在下班前解决这个问题是不可能了,通宵是必然了。

简化

Zen是组里的Tech Lead,今天在忙另外一个事情。这个周五真是不得安宁,恨不得想到美国去过过昨天。

Zen听到两个人的讨论,虽然并不了解这个问题的细节,但直觉上认为是跑偏了。马上提醒Ben和Martin:

这不是一个很难的问题,我感觉你们想复杂了?是不是走偏了?能给我说一下你们怎么想的么?

被Zen打断的Martin说了一下之前的解决方案,也说试过了其他的方案了,都不行。由于Zen对这个事情也不是很了解,所以只是提了一个醒:

“Keep it simple,别把事情整复杂了。”

两个人的讨论依然在继续,Ben有点无法跟上Martin的思路,艰难地写着代码,但每次都不对。Pair的气氛犹如冬日里冰冷的咖啡一样凝结,不知道孰是孰非。Ben已经有些不高兴,Martin则依然在一旁指指点点但并不动手。

Zen一看表已经3点钟了,又插了一句嘴:

Martin,既然你对这个更熟悉,你来操刀吧。你来写代码吧。

可能由于之前的讨论过于激烈,Martin反驳Zen:

我们在Pair啊,他对RxJava不熟悉,我应该指导他。我看着他写就可以了。

Zen说,

你们的解决方案是什么,给我看看。

解释了一通以后,Zen也没有更多的想法,就让他们继续吧。但Zen建议道:

在这个紧要的关头,我们应该改变一下Pair的方式。现在不是教授知识,而是要高效的解决问题。在这种压力的情境下,你可以直接实现自己的思路,带着别人飞就好了。

分歧

Martin稍微冷静了下,拿过键盘,继续开始修复问题。Ben这时候在一旁观察,也适当的休息一下,之前手忙脚乱的按F8、F9的神经也得以缓和。

Ben:看来还是不行。我们再理一下代码吧。

Martin:你说的这些我之前都试过,都不行,要这样才行。

Ben:我说的是这样做的,既然我们还没讨论清楚,我们再来看一下代码吧。

两人拿出了纸和笔,对着屏幕一边画一边讨论,然而Ben并不认可Martin的方案,说要采用另外个方案。Martin则坚持认为这是一个可行的方案,得试试。Ben拿过键盘,准备按照Martin的方案写代码,但心里面颇为不爽,一直在想说服Martin采用他的方案试试。

怒气

到此时,时间都已经不知不觉过去两个小时了,然而问题似乎离真相总是忽远忽近。两个人已经疲劳不堪,再加上解决方案的不一致,两人的言语中开始显露出一些怒气。

Zen在运行测试的空档,打断了两人的对话,建议道:

既然大家已经产生了分歧,要不然两个人分开,各自实现一个,看谁能够先实现,然后再来讨论。

Martin对于Zen并不认同,认为Zen指责他和Ben没有Pair好。

Zen解释道:

其实我听出了两人意见的不统一,言语中已经有一些怒火,这样下去Pair的效率很低。首先,大家带着不爽来干活,互相质疑。更关键的是,解决问题已经用去了两个多小时,大家都比较疲惫,可以适当休息。我让你们分开的目的是让大家冷静一下,在不受打扰的情况下工作一段时间,可能会不一样。

冷静

Martin回到了电脑面前,按照他的思路一步一步做下去。Ben去上了个厕所,倒掉了那杯冷冰冰的咖啡,泡了杯热茶。回到电脑前专注的重新按照他的思路一步一步走下去。

其实两个人已经接近了真相,只是这之间不停的对话把注意力消耗殆尽。两人企图达到一个统一,然而口头的对话并不能解决问题,反而暂缓了这个过程。

10分钟后,Ben兴高采烈的说已经搞出来了一套可以运行的方案,叫Martin一同过来看看。Ben的临时解决方案比较简单好理解,但并不完美。熟悉RxJava的Martin指出了一些可以改进的地方。

然后两人又开始了新一轮的Pair,重新将这个方案完善。有了这个基础的解决方案,两人都很高兴,是朝着一个正确的方向大步向前。

尾声

下午6点半,虽然比正常下班晚了半个多小时,但还好整个解决方案都正常了,交付的任务也顺利完成。

Ben和Martin都总结道,我们应该停止结对,当:

  • 两人的思路不统一但无法说服对方时:我们可以考虑分开一阵,安静一下,各自用可运行的代码来证明思路的可行。这里只需要相对粗糙的代码即可。
  • 时间已经超过番茄时间而感到疲惫时:人的专注力是有限的,在Pair时非常累,特别是在能力方面存在较大差距的时候。在这时候我们可以试试番茄工作法,让大脑得到休息。
  • 注意力不集中或者有其他事务要处理时:在Pair的时候,彼此要尊重对方,不要玩手机、看其他无关的网页,除非事先取得别人的同意,否则就要等到停止结对、处理完事务后再继续。

更多精彩洞见,请关注微信公众号:思特沃克

Share

测试矩阵

测试的种类繁多,难于理解,难于沟通。我觉得主要是在于我们将两个测试分类的维度混杂在了一起。

如果我们不再提“单元测试”、“性能测试”这种含糊不清的概念,而是通过测试矩阵上的二维定位法,改称“方法级别的功能测试”和“API级别的性能测试”,我想我们对于测试的沟通讨论甚至学习实现将明确的多,也简单的多。

迷阵

“单元测试,集成测试,端到端测试,安全测试,性能测试,压力测试,契约测试,冒烟测试,验收测试,API测试,UI测试,兼容性测试……”

不知道你是不是像我一样,曾被这些各种各样的“测试”搞得晕头转向。作为一个有追求的开发人员,保证所写的程序、所构建的系统具备良好的质量自然是分内之事。但是面对这些千奇百怪的测试难免会望而却步,只能劝自己一句“专业的事情还是交给专业的人去做吧”,然后把测试的工作一把推给QA,闷头写自己的代码去了。

不光是测试种类众多,每个人对于某一个测试的理解也都不一样。就拿大家最熟悉的“单元测试(unit testing)”来举例,问题的关键就被聚焦到了“到底如何才算是一个单元(unit)?”有人说是一个方法,有的人说是一个类,有的人说都不对,应该是一个最小的业务单元(至少是API级别的)。还有人提出了Integration Unit Test的概念,即集成级别的单元测试。

不光是我等软件小辈,就连很多IT界的神级人物也常常为此争论不休。

古话说的好,一千个人心中有一千种单元测试,看来说的是有道理的。

列表法

(列表法)

这是昨天陪闺女写作业的时候,看到她使用了一种被称作“列表法”的方法去解一个小学2年级的逻辑题。闺女说,这种方法很神奇,原本看起来弯弯绕的问题,画个表勾勾叉叉就解决了。

随后我也查了一下:“列表法是小学数学学科中经常使用的一种方法,使用列表法可以解决许多复杂而有趣的问题。运用列出表格来分析思考、寻找思路、求解问题,经常用来解决类似于鸡兔同笼的经典问题……”

虽然我一直没有搞清楚为啥要把鸡和兔子放到一个笼子里,但回到测试迷阵的问题,好像这种小学3年级就教授的方法也能适用。

测试矩阵

(测试矩阵)

测试的种类繁多,难于理解,难于沟通。我觉得主要是在于我们将两个测试分类的维度混杂在了一起。

其中第一个维度是测试实现的层次或粒度,说白了就是在哪个层次上的测试,也可以理解成测试到底测的是哪儿。是方法?是类?是API?是单个Service?是两两Service?还是应用?还是系统?还是平台?

我们常说的单元测试,API测试,端到端测试,UI测试都是侧重于按照这种维度去分类不同的测试种类的。

但是我们在谈论这些测试的时候,其实隐含了一个概念就是他们测的是什么?也就是测试的目标。例如当我们提到上面的单元测试、API测试、端到端测试的时候其实隐含的想表达的是单元级别的功能测试,API级别的功能测试和端到端级别的功能测试。

这时候你肯定会想,这不废话么,不测功能我测什么?

这就是我想说的第二个测试分类的维度:我们测试的标的物,或是说测试的目标。如果说第一种测试维度是根据“测哪儿”区分的,那第二个维度就是根据“测什么”区分的。

例如,我们常常提到的:功能测试、集成测试、性能测试、安全测试、压力测试、兼容性测试,契约测试都是这种按照这个维度去区分不同的测试种类的,他们都不是关注于我们要测哪儿,而是更侧重于我们到底要测什么:业务功能是否正确?是否能按预期集成?契约是否被保证?安全能否达到要求?性能是否满足预期和要求?

只不过我们日常工作中,大多数情况下测试都是在验证功能是否正确,所以我们常常忽略了第二个维度,只关注于测哪儿。只有当我们去测试像性能和安全这种非功能需求的时候才会想到第二个维度,但有趣的是往往我们这时候又会忽略第一个维度,例如当我们听到有人提及性能测试的时候,并没有明确的表达测的是方法的性能、API的性能,还是UI的性能,进而导致了理解的不一致和混乱。

换个叫法

可见,之前之所以被测试迷阵困扰,其本质原因就是并没有明确区分开这两个维度,甚至将之混为一谈,从而使我们对于“XX测试”的定位和理解包括沟通都变得模糊而不准确。

如果我们不再提“单元测试”、“性能测试”这种含糊不清的概念,而是通过测试矩阵上的二维定位法,改称“方法级别的功能测试”和“API级别的性能测试”,我想我们对于测试的沟通讨论甚至学习实现将明确的多,也简单的多。


更多精彩洞见,请关注微信公众号:思特沃克

Share

保持现状与有意为之的无知

科技与人文社科的失联,会导致整个左翼运动陷入一种尴尬的境地:对于资本用以牟利并同时制造社会不公的科技工具,科技工作者看不到其社会危害所在,人文学者又无法提出有效的批判和改进方向。无形之中,双方对于对方专业领域的有意为之的无知,都在帮助保持当前科技-社会结构的现状。要打破这种现状,需要双方都开始努力了解对方的专业领域,包括——我今天特别想强调的——人文社科学者学一点编程和人工智能技术。

1970年9月,萨尔瓦多·阿连德当选智利总统。智利人民用自己的选票,选择了他倡导的社会主义路线。执政之后,阿连德政府开始收购智利最重要的工业企业,将它们纳入国家控制。到1971年底,国家开发公司已经必须负责指导下属150多家企业,包括智利20家最大企业中的12家。国有经济的高速发展创造了一个笨重的、智利政府从未见过的怪兽,管理已经成为国有化进程的一个核心问题。为此,阿连德政府联系到了英国控制论学者斯塔福·比尔。比尔发现,控制论中关于反馈与掌控的思想能够指导开发一套新的科技系统来改善国有经济的管理,从车间直到国家开发公司办公室。这样一个系统将会搭建起实时信息交换的网络,管理者和政府官员将能够基于实时数据来做决策,并能够快速调整行动。这个系统,就是传奇的Cybersyn,距今半个世纪前出现在智利的大数据系统

按照比尔的构想,这个基于他的“自由机器”和“可生存系统模型”理论构建起来的大数据系统,将能够兼顾国家经济整体方向的一致性与企业的自主性,并且充分调动一线工人参与企业管理体制的设计与执行。然而在Cybersyn系统实施的过程中,智利科技专家们的实践与政府的政治理念并不吻合。虽然阿连德坚持要系统鼓励工人参与管理,但工人在Cybersyn实施中扮演的角色实际上是被边缘化的。更多时候,技术官僚主义在基层车间压倒了意识形态。尽管收到明确的指示要与工人委员会协作,但工程师们经常并不这样做,而是带着优越感看待工人,或是完全忽视工人、只和管理者打交道。

1973年9月,皮诺切特的军事政变推翻了智利社会主义政府,阿连德本人丧生于总统官邸。政变之后,军队中止了Cybersyn项目,团队的工作成果要么被抛弃、要么被破坏。在新的军政府和新自由主义“休克疗法”背景下,Cybersyn没有任何意义。然而客观地说,即使没有军事政变,Cybersyn是否就能如起初设计的成为对劳工赋权、鼓励工人参与管理、兼顾民主与集中的信息系统,比尔对此也并非没有怀疑。为何科技系统——甚至是那些原本为了革命的目标而建立的科技系统——常常倾向于维持社会与经济的现状,这是Cybersyn留下的值得反思的若干问题之一。

1973年,比尔反复思考了Cybersyn遭遇的各种问题,包括项目团队在科技上花的心思多过组织变革、智利工人没能用Cybersyn来辅助生产组织和管理等,并把自己的思考写成了《现状》(Status Quo)一文。他在文中写道:

对于马克思而言,资本是邪恶的敌人;对我们而言,资本仍然是邪恶的,然而敌人是保持现状。

比尔认为,科技的发展,尤其是通信与计算机领域的发展,使得资本主义已经发展出了新的生产形式与新的剥削关系。在这个新的关系中,不仅有资本家与劳动者的对立,受过高等教育的专业人士扮演了一个重要的角色。比尔从控制论的角度指出,官僚体系总是偏爱保持现状,而专业人士扮演的则往往是保持现状的力量、而非推动革命的力量。

仍然以Cybersyn为例:尽管顶层设计把它视为一个“革命的装置”,但在科技团队内部,很多人认为应该把意识形态放在一边,专注于科技性的目标,例如提升政府监管经济的能力、解决经济的效率问题、消灭官僚主义。Cybersyn项目主管埃斯佩霍说,很多科技专家想要加入这个项目是因为它“充满智力挑战”,这些科技专家对于科技与政治之间的关系有着不同的解读,并非所有人都赞同阿连德的政治理念。这个团队得以持续“健康”运转的基础,也许就是——如埃斯佩霍所做的——搁置意识形态的目标,专注于科技的目标。于是,专业人士团队基于自身利益角度出发的“求生意志”,就成为了一种保持现状的动力。

时隔四十多年以后,我们在今天的科技-社会的讨论中看到,这种来自专业人士角度的保持现状的动力变得更加强大,甚至时常被称为“科技本身的逻辑”(凯文·凯利还专门写了一本书来讨论“科技要什么”)。比尔在1970年代的反思让我们看到,这种“科技本身的逻辑”,经常是来自专业人士有意而为之的对意识形态、对社会问题的搁置。专业人士倾向于将自己的工作描述为纯粹科技的、“政治中立的”,使得自己不必接受“我的工作对社会有何影响”的追问。在快播案、魏则西案等一系列关于互联网伦理的讨论中,我们皆听到了这种纯粹科技论的辩解。我把这种“将自己的专业工作与社会/政治/伦理问题划清界限”的努力,称为“有意而为之的无知”(minded unmindedness)

这种有意而为之的无知,部分出自科技本身的复杂性与抽象性。例如广为讨论的人工智能技术,无论是向读者推荐视频、还是在读者的搜索页面显示广告,从技术的角度都可以归约为一系列在高维矩阵上进行的线性代数运算(以及与之相关的特征工程、算法优化等工作)。这种高度的复杂性与抽象性,使得科技专业人士能够埋头于诸如“计算稀疏矩阵中向量间的欧氏距离”这样的纯技术问题,而毫无愧疚地无视技术的应用对社会产生何种影响,并且在面临来自人文社科领域的置疑时轻易地给自己构建起坚固的保护壳。

然而问题并非只出在科技专业人士这一边。人文社科领域的专业人士同样有自己的有意而为之的无知,表现为对新技术的盲目恐惧,或者说是“将自己的专业工作与科技问题划清界限”。于是我们看到,来自人文社科领域的关于科技伦理的讨论经常流于表面,例如用科幻小说的方式讨论“强人工智能”,而缺乏对机器学习、神经网络等核心技术及其应用场景和局限性的基本了解。其结果是,来自人文社科领域对新科技的批评要么“脱靶”,要么在科技人士实用主义的反问“那你说该怎么办”面前黯然失语。像Cathy O’Neil这样能准确地指出科技系统中问题所在、能提出行之有效的解决方案、能持续量化监督科技公司改进的跨学科左翼人士,实在是太稀缺了。

解决这个困境需要科技与人文社科两边专业人士的共同努力。科技的专业人士当然需要更多地了解社会的问题及其渊源、更多地反思自己工作与社会/政治/伦理问题之间的关系。另一方面,我在这里想强调的是,人文社科的专业人士应该打破自己对新技术的盲目恐惧,不能坐等科技专业人士的觉醒,他们需要立即开始学习编程和人工智能的基础,使自己掌握有效批判的武器。

实际上这两项技术的门槛比很多人想象的要低得多。除了克服入门时的恐惧与不适,Python编程需要的理科知识基础约等于0——我曾经与同事半开玩笑地说,我们开发的软件只需要小学高年级数学水平,四则运算都用不全,主要是除法不怎么用。另一个学习编程的门槛是英语,然而人文社科领域的年轻学者大多具备相当良好的英语读写能力。自学一门编程语言(例如Python)这件事,我认为每位人文社科学者应该都能做到。

人工智能技术所需的理科基础则更高一些:如果想要比较深入地了解其原理(而不止是使用几个工具),需要微积分和线性代数的基础知识。以高中水平的数学能力,在一学期时间里重新捡回这两门课应该是可以做到的。(听说一些高校的文科院系大一已经不上高数课,我认为这是一个错误的导向。)

除了这一点数学基础以外,大部分数据处理和机器学习算法可以说是出人意料地简单。John Foreman的Data Smart一书教它的读者用Excel(是的,你没看错,就是你每天用的Excel)实现分类、推荐、预测等典型的机器学习算法,我认为这本书非常有助于破除笼罩在“人工智能”这个概念之上的神秘感。另外我也强烈推荐人文社科学者在学了一点Python基础之后尝试一下华盛顿大学的机器学习公开课。学完它的第一门课程,你就会发现,机器学习(乃至“人工智能”)其实是一件很简单、毫不神秘的事情——这一点,对岸的科技工作者们其实一直都知道。

科技与人文社科的失联,会导致整个左翼运动陷入一种尴尬的境地:对于资本用以牟利并同时制造社会不公的科技工具,科技工作者看不到其社会危害所在,人文学者又无法提出有效的批判和改进方向。无形之中,双方对于对方专业领域的有意为之的无知,都在帮助保持当前科技-社会结构的现状。要打破这种现状,需要双方都开始努力了解对方的专业领域,包括——我今天特别想强调的——人文社科学者学一点编程和人工智能技术。


更多精彩内容,请关注微信公众号:软件乌托邦

Share

浅谈微服务架构中的鉴权体系

在微服务架构中,有一个核心的问题是处理好“集权”(中心化)和“放权”(去中心化)的关系。虽然微服务的主旋律是把数据和业务拆成小而独立的模块,但我们仍然需要一个强力的中央安保体系来确保“数据分散,权限集中”。这一篇就谈谈微服务架构中的鉴权体系。

在微服务架构中,有一个核心的问题是处理好“集权”(中心化)和“放权”(去中心化)的关系。虽然微服务的主旋律是把数据和业务拆成小而独立的模块,但我们仍然需要一个强力的中央安保体系来确保“数据分散,权限集中”。这一篇就谈谈微服务架构中的鉴权体系。

身份认证

身份认证(Authentication)的目的是证明“你是你(所号称的那个人)”。

要证明这一点,你必须掌握一个只有你自己和认证机构才知道的机密信息。在现实中,这个信息可能是 DNA、指纹、虹膜这样的生物识别特征,但由于这种特征跟人身直接绑定且又不可修改,一旦泄露,可能被持续冒用,造成不可挽回的严重后果,所以现实中较少采用这些生物识别特征作为识别之用。

如果不采用机密信息作为判断标准,就需要一个持续的、不易伪造的“证明材料”。在中国,这个证明材料就是户口或身份证。中国对公民信息的登记相对严格,所以会在小孩出生的时候要求把身份信息登记到户口之中,形成身份证明,跟随一生。在需要证明“你是你”的时候,拿出身份证就行了。

(生物特征不可废弃,所以我们必须把它包一层,形成证明材料和对应的 Persona)

与生物识别特征不同的是,身份证如果丢失,从理论上说,应该可以挂失并且让其失效,然后办理一张新的身份证。不过,设计我国身份证的机构和供应商也许没有考虑到这个问题,或者考虑到现实情况太复杂,导致身份证无法挂失,丢失的身份证仍然具备证明效力,但这个是后话了。

为了避免身份证被冒用,在对身份认证要求比较严格的场合(比如银行),会附加一些别的检查,比如对比照片等等。

那么,现在我们来对身份认证进行规划。

  • 身份认证机构可以颁发两个东西给用户,作为身份认证的输入:机密信息或证明材料。
  • 身份认证机构可以通过对比用户提交和机构保存的机密信息来判断用户身份。
  • 身份认证机构可以通过检查和对比用户提交的证明材料来判断用户身份。
  • 如果需要,身份认证机构可能会附加别的验证来增加认证可信度。
  • 用户可以变更机密信息,避免冒用。
  • 用户可以挂失证明材料,使证明材料失效,避免冒用。

身份认证中的机密信息在 Web 环境中通常以用户名和密码的形式存在。由于 HTTP 协议没有“状态”的概念,所以对于 Web 服务器来说,每次请求都是全新的体验,都必须验明请求者的正身。要做到这一点,客户端可以在每次请求的时候都附上用户名和密码(或者别的凭据),表明身份。

可是,每次都发送用户名和密码增加了泄露风险,所以在第一次验明正身(登录)之后,服务器可以发给调用者一个“令牌”(Token)。这样,后端的后续身份认证,无外乎就是把令牌换成“身份”(Identity)。这个令牌实际上就是前面说的证明材料。

我们应该尽量让令牌不容易仿造,但是技术上无法做到完全杜绝。所以,在敏感操作的时候可能会附加一些别的验证,比如再次输入密码或者用短信验证码做二次校验,这也就是前面所说的附加验证。

权限验证

和身份认证相比,权限验证(Authorization)要复杂一些。

身份认证的输入,要么是用户名和密码(或别的身份凭据),要么是令牌,只需要通过一个检查,就能输出身份信息。而权限的验证要检查的是“某用户能不能做某事情”,所以,至少需要有两个输入:“用户身份”和“想要执行的动作”。除了这两个输入之外,还需要有一个具体的“判断规则”,验证者才能根据规则,输出“同意”或者“拒绝”。

在现实中,这个判断规则有很多种可能。

  • 在等级森严的军队里面,所有的动作和文档都有明确的“查阅级别”,而每个人也有自己的“查阅级别”。只有用户的级别高于动作的级别,才能执行这个动作。
  • 在分工明确的工厂里面,每个人都只负责自己的工作,那么,所有的动作和资源都按照不同的工种来进行分配。各工种只能执行属于自己负责范围的动作,获取属于自己负责范围的资源。
  • 在架构明确的公司里面,每个人都属于公司行政架构中的一个节点,可以执行属于这个节点的动作,并且访问这个节点及其下属节点的信息。
  • 在专家主导的医院里面,所有人都围绕专家的需要服务,而专家则为病人服务(执业)。根据专家的需要,同时保护敏感信息,我们可能会设置更加复杂的判断规则,比如根据时间段、服务流程阶段等来判断,或者提供一个特定的委托授权的流程用于临时开放权限。

不管怎么变,只要有了身份、动作和规则,我们就能做判断。当然,如果规则要求我们核实部分数据,我们还需要这部分的数据作输入。不过,由谁来执行这个判断比较合适,值得我们探讨一下。

举一个生活中的例子。

有这么一家公司,在 A 市有个办公室,办公室有个戴经理。戴经理有一天兴之所至,想起来要查一下员工老王的工资。他来到了 HR 部门,找到了 HR 主管,想要调老王的工资出来看看。

HR 看了看公司规定,经理只能看自己所辖办公室的员工工资,然后又看了看戴经理,是负责成都的,再看看老王,是成都员工,然后,就把老王的工资调出来给了戴经理。戴经理看了,然后说,再给我看看老陈的工资啊。然后 HR 调出档案一看,老陈是北京办公室的,就拒绝了。

又有一天,员工老王也兴之所至,想要查一下戴经理的工资。他也来到 HR 部门,找到 HR 主管,问戴经理的工资。HR 一看,你这不是经理啊,怎么能查别人工资呢,就直接拒绝了。

如果看这个例子,我们就会发现,这个规则的检查是 HR 做的。实际上,绝大部分非 IT 的业务流程中,权限的检查都由信息的保管方来执行。

我们当然也可以按照这个来建模,但是稍等,再深入分析一下。

  • 首先,“谁能看谁的工资”这个规则,是不是 HR 部门来决定的呢?不是。公司的规章制度决定了“谁能看谁的工资”,规章制度由公司管理者制定。
  • 然后,当公司制度需要调整的时候,是不是由 HR 部门来调整呢?不是。 还是由管理者来制定,然后由各个部门来执行。各个部门实际上是收到了制度调整的结果,而不是自己去调整制度。
  • 最后,“谁能看谁的工资”这个规则,是不是 HR 的专业范围?不是。只有“调出工资档案”这个动作是 HR 的专业范围,至于“谁能看”,其实跟 HR 的专业知识没有直接关系。

要理解最后这一点,我们可以看两个场景。

  • 某 HR 换了一家新公司。现在这个新的公司很有意思,允许所有人看所有人的工资,层级也不同,规章完全不同,但 HR 仍然可以按照自己的专业来工作不受影响。不只是 HR,对其他部门的人来说也是如此。规章制度的变化,对它们的职责没有实质的影响。
  • 某 HR 换了一家新公司。这家公司专门搞高精尖的研究,对于员工的信息和商业机密控制极其严格。只要有人来调取数据,都必须经过专门的审核人员审核放行。这对 HR 的职责也没有实质影响,只要能通过审核,照办即可。审核人员也不知道 HR 具体的工作是什么,只知道规则要求检查什么,就去检查什么。

总结就是以下几点。

  • 专业知识(领域逻辑、业务规则)和权限是相对独立的东西。
  • 要运用专业知识,不需要知道权限。
  • 要检查权限,不需要用到专业知识。

既然如此,为什么在现实中还是由专业人士来兼职检查权限呢?这也许是因为对于很多公司来说,绝大部分的数据都没有那么敏感,所以为了降低管理成本,绝大部分的数据访问都没有那么严格地用专职人员去检查,而是由专业人士代劳。

了解了这些之后,我们就可以开始规划了。

  • 权力机构会制定一套权限规则,并且可能调整这套规则。
  • 这套规则可能会用到一些外部的输入,比如员工所在的办公室。
  • 有了这套规则和查验数据的权力,任何人都可以判断一个动作是否合规。

这样做的好处,就是业务变得非常纯粹,而权限相关的东西完全挪出业务层面,即便业务或者权限需要频繁变化,问题也不大。

说到这里,也顺便抛一个待验证的设想:不同公司的业务逻辑总是高度雷同,差别最大(妨碍复用)的其实是公司的管理体系。我们把组织结构和与之相关的安全权限单独拎出来,也许可以更好地促进业务逻辑的复用。

鉴权服务

为了提升服务的效率,我们一般会希望尽早地做完身份认证和权限验证。如果用户执行了越权操作,那我们应该及早中止访问并返回错误提示。

前面提到,权限验证的输入之一是用户身份,所以身份认证和权限验证通常是前后脚来做。二者组合,形成鉴权服务(Auth Service)。鉴权服务负责维护令牌身份映射以及权限规则,它的输入是“令牌”和“想执行的动作”,输出是“身份”和“是否允许执行”。

几个例子

现在,我们用这样一个场景来实验一下整个鉴权流程。

设有这么一个订单管理系统,其中有一个订单查询功能。其权限要求如下。

  • 买家只能看到自己下的订单
  • 卖家只能看到下给自己的订单
  • 卖家管辖多个小二,小二可以分组给不同的权限,有的只能看分配给自己的订单,有的可以查看分配到自己组的订单
  • 运营商可以看到所有订单

要对这个权限体系进行建模,我们必须认识到,这些操作,虽然查看的都是订单,但是因为是不同的业务上下文,表现到 API 呈现上也会有不同。

  • 买家看自己的订单:/CustomerViewOrders
  • 卖家看自己的订单:/MerchantViewOrders
  • 运营商看任意订单:/AdminViewOrders

然后,我们可以制订如下规则。

  • 所有这些 API 都要求用户处于已登录状态
  • 对于 /CustomerViewOrders ,访问者必须有 customer 身份
  • 对于 /MerchantViewOrders ,访问者必须有 merchant 身份
  • 对于 /AdminViewOrders,要求当前用户必须有 admin 身份

这样,鉴权服务就可以根据身份、动作和规则三者来判断访问权限了。

(图片来自:http://t.cn/RHRujGG)

至于卖家给小二的权限分配,根据不同需要,我们可以选择两个方案。第一是让卖家自己去处理这个细粒度的权限,形成自己的一套小的权限体系,这也意味着小二访问的可能是因卖家中转而暴露出来的新 API。第二是把这个细粒度的权限也建模到原来的权限体系里面,加入如下新的 API 和判断规则。

  • 小二查看订单: /ClerkViewOrders,检查:
    • 用户必须是 clerk 身份
    • 用户在组织结构上必须属于某个 merchant
    • 如果用户类别是 1,那么他可以查看所有分配到自己组内任意小二的订单
    • 如果用户类别是 2,那么他可以查看分配给自己的订单

我们再来看另一个场景,查看员工信息。API 的和规则的设计如下。

  • 所有 API 要求用户处于登录状态
  • 员工查看自己的信息:/EmployeeViewOwnProfile
    • 所有员工均可访问
  • 员工查看其他员工的信息:/EmployeeViewProfile
    • 所有员工均可访问
  • 经理查看员工信息:/ManagerViewProfile
    • 当前用户必须为 manager 角色
    • 请求中的员工必须属于该 manager 负责的 location

不同的 API 返回的数据可能有差别,比如看自己的信息可以看全,看别人的只能看名字、照片和联系方式,经理则可以看所有人的完整信息,这由应用逻辑决定。

(图片来自:http://t.cn/RHRu1e3)

再来看一个医院的。医院有一点不同的是,病人和病历实际上需要在多个部门之间周转,而不同的角色处在不同部门的时候,其职能和权限会有变化。比如, 有时候实习医生会守急诊室,住院医生不在的时候护士也需要代理执行医嘱,职工可能会轮岗到不同部门,等等。

基于这样一些假想场景,我们可能会有如下一些 API 和权限。

  • 挂号处,要求用户必须有 clerk 身份
    • 建档:/RegistrationsCreateMedicalRecord
    • 挂号:/RegistrationsCreateVisit
    • 查看病历(用于确认病人已建档):/RegistrationViewMedicalRecord
  • 门诊部,要求用户必须有 doctor 身份
    • 诊断:/OutPatientCreateDiagnosis
    • 开药:/OutPatientCreatePrescription
    • 查看病历:/OutPatientViewMedicalRecord
  • 急诊室,要求用户必须有 doctor 身份
    • 查看病历:/EmergencyViewMedicalRecord
  • 住院部,要求用户必须有 doctor 或者 nurse 身份
    • 入院:/InPatientAdmitPatient
      • 仅 nurse 可以执行入院
    • 日常检查记录:/InPatientCreateRoutineRecord
      • doctor 只能给自己分管的病人创建检查记录
      • nurse 只能给自己负责区域的病人创建检查记录
    • 创建医嘱:/InPatientCreateOrder
      • doctor 只能给自己分管的病人创建医嘱
      • nurse 不能创建医嘱
    • 出院:/InPatientDismissPatient
      • 仅 nurse 可以执行出院
    • 查看病历:/InPatientViewMedicalRecord
      • doctor 只能查看自己分管病人的病历
  • 手术室,要求用户必须有 doctor-surgeon 身份
    • 准备材料:/OpRoomPrepareMaterial
    • 记录结果:/OpRoomCreateOpRecord
  • 检查部,要求用户必须有 technician 身份
    • 录入结果:/LabsCreateExaminationRecord
    • 查看病历:/LabsViewMedicalRecord
  • 药房,要求用户必须有 pharmacist 身份
    • 看处方:/PharmacyViewPrescription
    • 放药:/PharmacyDeliverMedicine

上述 API 能访问到的数据和权限主要根据部门来进行划分,方便轮岗。比如,医生在门诊的时候,可以查看完整的病人病历,但轮岗到挂号处的时候,虽然也查看病历,但就只能查看最基本的个人信息了,用于给病人补办卡片之类。

功能和数据权限

从上面几个例子看来,我们通常可以把权限的验证分成两个步骤:先确定职能,然后确定职能作用范围。

比如,先确定你能看订单,然后确定你能看哪些订单;先确定你能看工资,然后确定你能看谁的工资。再比如,某国法律规定,当一个案件发生在某地,警察来调查,但只有该辖区的警察有调查权,跨区域的案件必须交给联邦警察。如此等等。

既然这两步看上去分得很清楚,那么我们不妨给它们分别取名。用户能不能执行某个动作,使用某个功能,是功能权限,而能不能在某个数据上执行该功能(访问某部分数据),是数据权限。

促成这种拆分方式的原因可能有下面几种。

  • 现实中,很多组织采取了这种“职能 + 组织节点”的形式来确定权限,所以这样的拆分实际上为建模提供了方便。
  • 由于功能权限通常会直接对应应用的 API 列表,所以权限验证可以及早失败,而无需把数据取出来做对比,提升了鉴权的效率。
  • 方便我们把所有的功能 API 提取出来形成一个列表或者表格,可以更好地查看和管理权限。

此外,这种形式的权限管理还可以让业务人员在不写代码的情况下对功能权限进行重新分配。如果涉及数据权限,则必然会有某种形式的判断逻辑,写代码也就必不可少了。

话说回来,尽管这种拆分很常见,我们仍应该认识到这只是人为的一种拆分。二者都是权限验证的一部分,都是为了回答“该用户能不能做某件事”这个问题,本质没变。

需要注意的是,在制定权限规则时,制订者需要参考业务规则,但是反之则不然。业务规则可以在完全不了解权限验证规则的情况下执行。甚至,从理论上说,所有的业务单元都应该可以在完全没有权限验证的情况下“正常裸奔”,即假设所有人可以做所有事情,但业务应该被正常执行,业务规则应该被正常遵守。用语言学的词汇来说,就是在没有权限验证的情况下,业务数据中也许会有语义问题(semantic problem),但是不会有句法错误(syntax error)。

鉴权体系回顾

我们来回顾一下这篇文章中提到的鉴权体系。

  • 身份认证。确认“你是你”,获取你的身份信息。
  • 权限验证。确认“你能做某件事”。

二者合称为“鉴权”。身份认证输入令牌,输出身份。权限验证输入身份、动作(包括动作范围),输出“同意”或“拒绝”。我们希望身份和权限在一个体系内高度一致,所以,鉴权是一个半中心化的行为,权限规则在一个体系(比如组织、应用)内是中心化管理的。

权限的形成需要对业务知识的了解,但规则抽象出来之后,要使用它就不需要业务知识了。权限验证的独立,意味着我们把“权限规则”和“业务规则”拆成了两个部分。前者拥抱变化,而后者追求稳定;前者在意的是业务的意义,后者在意的是业务的逻辑。

为了适应现有组织形态和更清晰地展示权限信息,在给权限建模的时候我们常常会把它拆分成功能和数据权限两种。我们应该认识到二者都是权限验证的一部分,都是为了回答同一个问题:这个用户能不能做某事。

从整个分析脉络我们可以看到,这个鉴权体系是通用的。在设计任意一个系统的过程中,我们都应该注意尽量把安全相关的判断和业务规则拆开对待,方便集中管理权限,把业务规则提纯。

对于微服务架构来说,鉴权是一个重要的节点,它和应用场景密切结合,是安保的最后一道关口。在对权限进行建模的时候,我们应该尤其谨慎。希望这篇文章能给大家一些启示。


更多精彩洞见,请关注微信公众号:思特沃克

Share