被踢出去的用户

0

在还没有掌握全部证据之前就下结论会犯严重的错误,会使判断带有偏见。——《血字的研究》

“齐识,路老板又来邮件了。”白娜一脸无耐地说。

“一定没好事吧?”齐识回头看了一眼,手依旧在快速敲着代码,并没有停。

“你刚从泰国回来?”齐识说到。

“你怎么知道?”白娜瞪着眼睛一头雾水。她清明假期去泰国玩这件事,并没有告诉任何同事。

“你脸明显比节前黑了,说明去了热带地区玩。你戴的耳环以前没看到过,说明是假期新买的。耳环上刻着大象……”,齐识停下敲代码的手,回过头,“说明你去了视大象为国宝的泰国。”

“你……牛……,不愧是程序员名侦探……”。

“说吧,又出什么事了?”齐识回到显示器前继续敲代码。

“还是用户被踢出去的那个问题。”

“上次不是已经查清楚了吗?是低版本IE的锅。”

“可是,这次是IE 11……”

“哎——”齐识长叹一声,“好吧,把邮件转给我。”

“已经转给你了。拜托了,我去开会了哈。”

齐识很不情愿地将界面从IDE切换到网页,打开了邮件。这是一封几经转发的邮件,一开始是路老板的用户向路老板抱怨系统用着用着就自动登出了你们这什么烂系统,路老板回复说可能是低版本IE的锅爸爸请考虑升级IE或换其他浏览器。客户说老子TM用的就是最新版IE你是不是在玩老子,于是路老板转发给项目经理白娜说今天必须解决否则炒了你们整个团队。最后白娜转发给齐识,只有三个字母:FYI……

很麻烦。这个问题以前也出现过几次,一直没有找到具体原因。上一次时发现用户用的是IE 8,而当时系统支持的IE最低版本是9。当所有人都焦头烂额束手无策的时候,这位超级英雄背锅侠IE站了出来。果不其然当用户换了其他浏览器后,就再也没有出现过类似问题了。

但这次,背锅侠说这锅老子不背了。

齐识看了看表,上午11点。不知道要为这个问题加班到几点。

他先打开了日志分析工具,按照用户名查询与该用户有关的日志。永远不要相信一个不懂电脑的用户所描述的事实,所有的信息都要再次确认。但这次事实似乎很无情无义无理取闹,这个用户用的确实是IE 11。

通过日志,可以看到这个用户所有的访问记录:他先是登录系统,打开几个页面,然后到了文件管理子系统,找到某个文件夹,上传了一个文件,然后就登出了。如果用户描述属实的话,这次登出请求不是他主动触发的,是系统自动把他踢出去了。

“用着好好的,怎么就被踢出去了呢?”齐识自言自语道。

“怎么了大侦探,愁眉苦脸的。”老夏端着泡了枸杞的保温杯路过。现在是11点半,这杯应该是第三泡。老夏以前也是项目上的大牛,后来不知怎的,去了培训部专门做员工培训了。不过他时不时还是会到研发部这边溜达溜达,从背后窥探一下别人的屏幕,指出代码上的一些问题,然后哈哈一笑扬长而去深藏功与名。

“用户又被自动踢出去了。”齐识说。

“又是那个问题?不是IE的锅吗?”

“看来我们有可能冤枉IE了,这次用户用的是新版本IE,按理说我们的前端是肯定支持的,也在测试环境验证过无数遍了。”

“这就怪了。这几次出问题的用户,是不是都用的IE?”

“是的。”

“那就还是IE的锅。”老夏吸溜了一口枸杞茶,走了。

他看似不经意的一句话,反倒提醒了齐识。他立马继续查询日志,找出几个用不同浏览器的用户,他们的各种操作错落有致,最后的登出请求也并无破绽。只有一些IE用户,他们的登出请求是在上传完文件之后发生的。难道是上传文件的后台接口出了问题?但不同浏览器产生了不同的行为,这现象又 不像是后台的事儿啊。齐识一看表,已经12点半了。得了,先去吃饭吧。

1

……权衡点滴证据,做出不同的假设,把它们进行对比,最后再确定哪几点是重要的、哪些是不真实的…… ——《巴斯克维尔的猎犬》

草草吃完,买了杯咖啡,齐识又坐到了电脑前。像这种线上问题,尤其是本地无法复现的线上问题,是他最喜欢的。定位这些问题的过程,就像是侦探在探案,从最初的案件出发,将看似毫无关联的线索逐一梳理过滤,那最终唯一的真相也会慢慢浮出水面。每到这时,齐识就会感觉像是个超级英雄一样拯救了世界。这样的问题解决得多了,同事们给他起了个外号,叫“程序员侦探”。他倒也不介意别人这么叫他,反倒有点自豪。

但这次用户被踢出的问题,齐识前前后后处理过三次,都无法定位。按照目前系统的实现,用户自动登出可能是因为网站的登录cookie过期且SSO上的登录cookie也过期,这时当某个请求到达后台时,会清空所有与认证有关的cookie并重定向到SSO的登录页面。登录cookie的有效期是20分钟,但用户抱怨的是刚登录不久就被自动踢出,从日志上看也的确如此。所以“登录cookie过期”的不在场证明相当完美。

另外的可能就是缓存挂了。每个请求到达后台时,都会到服务器缓存中取出在用户登录时存储的一个token,将之与请求所携带的cookie中的token比较,如果不相符就自动登出。之所以这样做是考虑用户的安全,将伪造或窃取cookie登录的黑客拒之门外。如果存储或读取缓存失败,自然也会自动登出。齐识以前在读写缓存的地方加了很详细的日志,并没看到任何错误发生。“缓存”作案的可能性也不大。

最后一个嫌犯是心跳请求。网站前端每隔2分钟会自动向后台发一个心跳请求,如果服务器发现本次心跳与上一次心跳间隔时间超过3分钟,就认为用户已处于不活跃状态,自动将其登出。这么做也是为了用户安全,比如将所有网站页面关闭,3分钟后再次打开,将会自动跳转到登录页面。如果心跳请求没有发送成功,下次请求到来时很可能已经超过了3分钟,就会把用户踢出去。心跳请求可以在Web服务器的日志中查到,每次请求都是成功的。所以,“心跳”的嫌疑似乎也撇清了。

就在上一次处理这个问题时,齐识偶然发现请求日志里显示的UserAgent是网站不支持的IE 8。使用系统不支持的浏览器,任何诡异的事情都可能发生。当用户升级了IE或使用了其他浏览器后,问题不再发生了。但是这一次,为什么新版的IE也不行了呢?

现在,齐识的目光集中在了上传文件的后台API上。这是这次发现的新嫌疑人,不,还有IE,可能是团伙作案。现在掌握的最新线索是,用户使用IE,不管什么版本,在上传完文件后,被自动登出。对于IE这种惯犯,不容易找出它的破绽,所以齐识决定先从API下手。

2

在没有事实作为参考以前妄下结论是个很大的错误。主观臆断的人总是为了套用理论而扭曲事实,而不是用理论来解释事实。——《波希米亚丑闻》

两个小时过去了,还是没有任何进展。上传文件的API若无其事地待在那里,一脸蔑视地看着齐识。

“有新的线索吗?”老夏又端着保温壶过来了。现在是下午3点,壶里应该泡上了新茶。老夏喜欢在午饭后泡一壶酽茶。

“今天的普洱味道如何?”齐识问。

“不是普洱,是正山小种。”

“咦?红茶应该是周四泡的啊,今天周三应该是普洱才对。”

“这个嘛,突然就想喝红茶了,呵呵。”老夏说完吐了吐茶沫。“你有不错的洞察力,总是能发现别人不太在意的事情。但有时候,根据那些事实并不能推理出你的结论。有些事情之间的关联可能很偶然,并不具备规律性。”老夏微笑地看着齐识,“现在情况如何?”

“我现在在看上传文件的API。我发现每次用户调用完这个API之后就会被踢出去。”

“哦?有意思,这个API做了什么见不得人的事了吗?”

“并没有。我已经检查了它和它所调用的所有方法,甚至还看了它本身的filter和全局filter,并没有任何地方会清空cookie或者token缓存。只有清空这些才会自动登出。”

“对了,你是怎么发现调用完这个API后会被踢出的?”老夏接着问。

齐识调出了某个用户所有请求日志,定位到上传文件的请求。“你看,这个POST请求是上传文件,后面这些就是登出请求了,而前面的都是一些文件夹跳转的请求。这是其中一个用户的请求,这里还有其他用户。”说着,齐识又开了几个窗口,平铺在了显示器上。

“有意思。”老夏眯缝着眼睛紧盯着屏幕。“虽然他们都是上传完文件就被踢出了,但能确定就是上传文件的API导致的吗?是每次上传完文件都会登出吗?”

“还真不是!”

“而且别忘了IE,只有IE才会踢出用户不是么?那就更能洗清后台API的嫌疑了。”

“对啊老夏,我怎么没有想到?”齐识茅塞顿开。

“就像我连续两周都是周三喝普洱周四喝红茶,你认为那是某种必然。但这是你的主观臆断。主观判断一定要有事实作为依据,否则就都是臆断。”老夏说着,拿起茶壶晃晃悠悠地走了。

“等等,老夏,别走啊,我需要你!”

“我还有事。”老夏回过头冲齐识眨了眨眼睛说,“等你再需要我的时候我自然会出现的。”

3

他的表情不再那样淡漠,我看到他炯炯有神的双眼迸射出智慧和兴奋的光芒。——《格兰奇庄园》

齐识的目光又聚焦在了日志身上。如果用户真的在上传文件之后被踢出,一定能从日志中找到什么蛛丝马迹。齐识又换了一个留下的操作日志较多的用户,将日志按时间排好序。这个是登录成功的请求,这个是进入文件管理子系统的请求,这个是进入某个文件夹的请求,这个是心跳请求,然后上传文件,成功了,然后跳转到其他文件夹,继续上传文件,又发了个心跳,看看离上次心跳的间隔,嗯,2分钟,没有任何问题。齐识点击进入了下一页日志。

又进入了另一个文件夹,还是上传文件,成功了,然后……就登出了。这里面一定有什么猫腻。

齐识揉了揉眼睛,将所有注意力都集中到这几行日志身上,仿佛要看穿这屏幕,看到屏幕那头的另一个镜像的宇宙。

用户在15:32:26进入了第一个文件夹,15:32:45发送了第一个心跳请求,然后上传了第一个文件,15:33:20跳转到第二个文件夹,15:34:30上传第二个文件,15:34:45发送了第二个心跳请求,15:35:07进入第三个文件夹,15:37:48上传第三个文件,15:37:49,用户被踢出。

齐识目不转睛地盯着每个请求的发送时间,突然,他炯炯有神的双眼迸射出智慧和兴奋的光芒。用户在15:37:48上传第三个文件之前,丢失了一个心跳请求,这个心跳本应该在15:36:45发出来。

心跳怎么断了?如果是心跳断了,是必然会被踢出的,这样是解释得通的。但心跳怎么能断呢?齐识打开发心跳请求的JavaScript文件,就是一个简单的setInterval,没有什么特别的。是什么,让这个2分钟的轮询停止了呢?

老夏呢?老夏呢?这老家伙跑哪去了?他说过会在我需要的时候出现的,现在我需要你,可你人呢?我发现了重要的线索,已经锁定了嫌犯,现在就差证据了,就差证据了。

4

排除所有不可能,剩下的那个不管多不可思议,都是事实真相。——《四签名》

“老夏?他说今天要去幼儿园接孩子,提前下班了。”培训部的同事说。

齐识的一脸兴奋变成了一脸沮丧,他本想告诉老夏这个重要的发现,然后跟他一起找到问题的根本原因的。但老夏他居然提前下班走人了。

“你要有事,就给他打电话吧。”培训部的同事看齐识如此低落,就提醒道。

对呀,我怎么忘了这个世界上还有电话这么神奇的存在。齐识拨通了老夏的电话,没有人接。

“老夏,我有了重要的发现……”齐识把刚才的线索编辑成一条微信,发给了老夏。然后回到座位上接着分析。

用户前两次上传文件都没有问题,可是第三次就被踢出了。从时间轴来看,前两次进入文件夹后,都是很快便发出了上传文件的请求,唯独第三次,进入文件夹之后,停留了2分多钟才发出了请求。用户在干什么?进入文件夹后去喝茶聊天了?那样的话心跳没有理由断掉啊。

齐识启动IE,打开开发者工具,登录本地的系统,进入文件管理子系统,打开一个文件夹。接下来该干什么?齐识发呆了几分钟,一个心跳请求发送出去了。他回过神来,点击上传文件的按钮,弹出了选择文件的窗口。上传哪个文件呢?该死,电脑里没有PDF文件。这个文件管理系统只能上传PDF文件,并且做了文件头校验,直接改后缀名是不起作用的。于是齐识只好搜了一个TXT转PDF的在线转换工具,丢上去一个空的文本文件,得到了一个PDF。然后切回刚刚打开的系统,选择文件的窗口还开着。他找到转换好的PDF,点击按钮。然后,然后浏览器就自动跳转到了登录界面……

什么?复现了?这怎么可能?这么轻易就复现了?发生了什么?

齐识又进入系统,上传刚才转换好的PDF,一切正常。

闹鬼了?齐识把刚刚所有操作的日志拿出来看,第一次上传文件——也就是失败的那次——的时间,比进入文件夹晚了2分多钟,比上次心跳请求晚了3分多钟。按照系统的实现,超过3分钟没有心跳请求,后台会认为用户已经不活跃,将其自动登出。也就是完全复现了生产系统中用户的问题。而在此期间,齐识是去转换文件了。

齐识盯着日志,良久之后,哈哈哈哈地笑出了声。

5

通过搜索,齐识很快验证了自己的想法。在IE下,像JavaScript引发的alert窗口或file组件打开的窗口,都属于模态窗口,它们会阻塞所有主线程中正在执行的JavaScript代码。至于Chrome、Firefox这样的浏览器,打开的并不是模态窗口,这也就是为什么只有IE频繁报出类似的问题,其他浏览器则一直表现良好。当这种模态窗口一直处于打开状态时,心跳请求就被迫中断了,继而在上传成功后,被自动登出。

证据确凿,“凶手”就是你了!

一定要把这个消息告诉老夏。齐识拿起手机,看到老夏半个小时前的一条回复:是不是上传文件的窗口打开的时间太长了?

齐识不仅震惊,更是钦佩得五体投地。他把刚才的经过,一五一十地发给了老夏。过了一会儿老夏回复道:“哈哈哈,果然,是哪个笨蛋用户打开了窗口3分钟都找不到文件?等等,不对,不是3分钟,是1分钟!”

“没错,所以问题出现得还是很频繁的。”齐识又和老夏聊了几句,突然想起来什么,就问:“老夏,你明明不姓夏,可是为什么大家都叫你老夏呢?”

“呵呵,这次解决问题,或者叫探案的过程你感觉怎么样?”老夏似乎有点顾左右而言他,”我看前两天你桌上放着一本福尔摩斯探案集,想必也是个Sherlockian,那你一定听说过那句话吧?”

世界上没有真正完美的犯罪,其实真相一直就在我们眼前,只不过还没有被发现。所谓推理,不过就是把重要的细节放大。

当齐识和老夏同时打出这句话时,两个人都笑了。

6

“那个,我以前做开发的时候,大家都叫我夏洛克”。


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

Share

ThoughtWorks:毕业生的“死亡矿井”

在古老的艾泽拉斯大陆东部王国的土地上,有一个名叫“月溪镇”的村庄。每个魔兽世界的联盟玩家几乎都在这里留下过足迹,因为这里有一个副本,叫做死亡矿井。当联盟玩家出了新手村,成长到12、3级左右的时候,就可以挑战死亡矿井了。这里有丰富的经验、大量的金币和足以让人脱胎换骨的装备。一般玩家都会选择找高等级的玩家带刷,10次、20次……甚至直到20级。但也有少数玩家,会召集5个小伙伴,一起挑战里面的各个boss,小心翼翼,步履维艰,但却十分享受……

1-deadmines

如果把刚刚离开大学校园的毕业生比作魔兽世界的人类玩家,在经历了北郡修道院(大学课程)的初出茅庐、艾尔文森林(公司实习)的小试身手,击杀霍格(毕业论文)的锋芒毕露,来到人烟寥寥的西部荒野(社会),下一站是哪呢?你该去哪里提升装备、练习操作,又该去哪里学习技能、磨炼意志呢?

熟悉游戏的老司机肯定会说,去刷“死矿”啊。

没错,如开头所说,“死亡矿井”这个副本正是为这样的玩家准备的。那么对于毕业生来说,什么样的公司具备这样的环境呢?作为一名在ThoughtWorks工作了三年的老骚客(ThoughtWorker),目睹过众多毕业生从生根发芽到野蛮生长最后尽情怒放的过程。我可以负责任地说,ThoughtWorks就是毕业生的“死亡矿井”。

为什么呢?因为她骨骼清奇、特立独行,是互联网时代的一股清流。

ThoughtWorks大学和思沃学院

ThoughtWorks最吸引毕业生的一点不外乎TWU(ThoughtWorks University)了。每一个来到ThoughtWorks的毕业生都可以去印度浦那参加为期5周的集中培训,和那些聪明、勤奋、富有激情的外国同事一起,共同学习公司文化、敏捷方法论和软件开发技能。

2-twu

不同背景、不同文化的相互冲击,往往能催生意想不到的化学反应。我曾见过很多毕业生从TWU归来之后,鱼跃龙门、破茧成蝶。

除此之外,你还有可能参加思沃学院(ThoughtWorks Academy)或郑大夜校的培训,帮你在真正上项目之前打下坚实的基础。

在刚刚结束的为期六周的思沃学院暑期特训营里,就有将近100名在校学生完成了步入社会之前的重要一课。

国际化的视角和眼界

在ThoughtWorks,你将获得其他公司很难拥有的国际化视角和眼界。

ThoughtWorks是一家跨国公司,拥有包括Martin Fowler在内的众多国际知名技术专家。这些专家每年都会聚在一起,经过几天的头脑风暴和激烈讨论,产生出一份独具特色的技术趋势报告——ThoughtWorks技术雷达。ThoughtWorks的项目在技术选型时都会参考技术雷达,同时,这些项目也会反过来影响技术雷达,为它提供实践基础。

因此,ThoughtWorks所使用的技术一直都在引领潮流。以前端技术为例,P项目从11年开始引入AngularJS,14年尝试ReactJS,而今年则开始Aurelia的各种趟坑。又如微服务架构,该项目从12年开始服务化拆分之路,Martin Fowler在13年底来北京考察,对我们的实践给予了充分肯定,并在14年3月份写下了那篇著名的文章Microservices。而当今年国内刮起微服务旋风时,我们早就开始在雷达中提醒大家不要盲目迷信了。

由于缺乏国际化的视角和眼界,国内很多公司容易陷入一些误区,比如对大数据的盲目迷信,比如Gitflow,比如用Jenkins搭建部署流水线……在ThoughtWorks,这些错误往往会被扼杀在萌芽状态。

所以,你可以一直站在巨人的肩膀上,看到的风景自然与其他人不同

4-giant

先进的软件开发方法和工程实践

在ThoughtWorks,你除了可以和诸多大牛谈(结)笑(对)风(编)生(程),还能学习很多先进的编程理念,掌握优秀的软件开发方法和工程实践。

我们一向推崇的工程实践包括:Clean Code、TDD、持续交付……你很难看到那些扛着云计算大数据机器学习乃至深度学习大旗的互联网公司谈论这些,而这些恰恰是摩天大楼的地基。我曾见过很多资深的开发团队不知道怎么写测试,更玩不转持续集成。没有这些基础的实践,如何做到高质量交付呢?

除此之外,对于敏捷的孜孜以求使得我们的很多项目可以成为业界的标杆。如果你不知道敏捷该怎么做,来咨询ThoughtWorks是肯定没有错的。所以,你一定能够在这里掌握敏捷方法论的精髓,从而在未来指导你的项目。

愿意帮助你成长的团队

ThoughtWorks对于新人的要求是宽容而严格的。

宽容在于对犯错的态度。人都会犯错,但不同公司对于错误的容忍度可谓天壤之别。在某些公司,可能会因为一个小小的错误就开除你,但在ThoughtWorks,犯错是被包容甚至是鼓励的,只要你是因为想学习而不是蓄意破坏。

严格在于,我们会每个月对新人进行组内考核,俗称interview++。如果团队其他成员认为你某些方面需要改进,这些信息会毫无保留地传达给你。

虽然看似严格,但其实团队的每一个人都特别希望看到你成长,并愿意尽一切可能帮助你成长。我们不会在意你最终会超过我们,相反,如果你能够超越我们,并在更大的舞台上展示自己,我们会因曾经在你成长的道路上帮助过你而感到骄傲

5-road

这种帮助可以到什么程度呢?即便是初出茅庐的毕业生,也有可能在试用期之后就成为team leader。是的,你没听错。在ThoughtWorks,这样的机会对每个人都是平等的。相比于经验和能力,我们更看重你是否有意愿快速成长。如果有,我们非常愿意尝试让你承担更多的职责,做出更多的贡献。

例如,W项目共有6-8个小组,每个小组大概7-8人规模。曾经有一段时间所有的team leader都是毕业刚满一年且在团队中资历最浅的新人。这样的胆魄不是所有公司都有的(除非资深的人都走光了)。而这些新人也不负众望,项目在他们的带动下有条不紊地推进着。如今他们也在ThoughtWorks或其他公司承担着更重要的职责。

此外,如果你足够优秀,很可能被每天都在写代码、业余时间做吉他的中国区CTO收为弟子,由他亲手培养。是不是想想都有点小激动?

参与项目的方方面面

今年上半年,ThoughtWorks的C项目举办了一次规模空前的技术大会。在三周的时间内分享了24个话题(其中18个来自项目内部),内容涵盖开发、架构、需求、测试、安全、运维等多个方面。

7-project

对于以上话题中的内容,团队的每个成员都能参与其中。是的,ThoughtWorks希望每个开发者都能成为全栈工程师,而且开放的文化和宽松的氛围也允许你随便修改项目里的任何一行代码。从前端到后端,从开发到运维。所以,你不会像在有些公司那样,工作多年却只涉及一两个模块,管中窥豹,只见一斑,无法在一个制高点鸟瞰项目的全景。

扁平的组织结构和轻松的工作氛围

在ThoughtWorks,虽然也有所谓的领导来负责公司运营和制定各种制度,但绝大多数的员工都是和你平级的。你可能会看到有人在拍桌子争吵,但那绝对是争论某个问题,对事不对人,更不是什么办公室政治。

这样的组织结构得以成为有志者的梦想平台。我们更看重的是影响力而不是职位,只要有人愿意追随你,就非常容易推动你想做的事情。比如,曾经有一位毕业生同事,非常喜欢体验设计,自己能力提升了之后,大家都很艳羡他的能力并愿意追随他的脚步,中国区的用户体验团队几乎就是他以一己之力建立起来的。还有前面提到的思沃学院,也是由几位自带教书育人光环的同事一手打造。

而工作氛围可以“轻松”到什么程度呢?下图为不修边幅顶着一头怒发的北京办公室运营总监在午休时和其他同事一起众乐乐玩FIFA。

8-xiaoqiang

当然,我们是专业的服务公司,每个人都具备高度的自律性,这些场景只会出现在休息时间。

积极倡导经济和社会公正

ThoughtWorks有三大支柱:经营可持续的业务、推动IT变革和追求软件卓越、积极倡导社会和经济公正。这是支撑ThoughtWorks商业模式的核心,是ThoughtWorks的基石,影响着我们所做出的每一项决策。

尽管“推动IT变革和追求软件卓越”似乎更像是一家有使命感的IT公司的slogan,但实际上最让ThoughtWorker们引以为豪的并不是P2(第二大支柱),而是P3——积极倡导社会和经济公正。ThoughtWorks每年都会从利润中播出专款用于社会公益项目,帮助弱势群体。比如我们曾经开发的一款专为听障人士服务的手机App心声,可以通过语音识别技术帮助听障人士与不会手语的健听人沟通、看电视、看视频和打电话。

ThoughtWorks拒绝性别歧视,员工的男女比例在IT公司里绝对属于较低水平。并且前面提到的思沃学院还在去年创建了“卓越女生实验室”,专门帮助希望在IT领域有所发展的在校女生。ThoughtWorks也不看重学历,有很多一般大学甚至大专学历的员工,依然能够在这里施展才华。

以上是我目前能想到的ThoughtWorks的几个优势,不一而足。看了这些,你是否也觉得这里就是那个能让你旧貌换新颜的“死亡矿井”呢?

来吧,来ThoughtWorks吧,和我们一起,Stop working,Start amazing!

Share

我为什么选择ThoughtWorks?

前一阵在知乎上看到一篇关于“选择某大公司还是ThoughtWorks”的提问,于是毫不犹豫地点了进去。果不其然,除了一些同事的安利回答(尤其以Phodal同学的安利最为严重)外,也没有太多有价值的参考了。对于这种一边倒的回答,我只想说我喜欢……额不,不是这句,应该是,很可能会有阴谋论者以为是营销套路吧?

其实选择哪家公司,并不是一件多么难以抉择的事情。如果选择了某家公司却发现不适合自己,那就趁早走人呗。我相信在就业上能够有所选择的人,是不会因为在一家公司待了几个月发现不合适,辞职出来后就找不到工作的。既然现在能拿到ThoughtWorks的offer,几个月或几年以后,只会拿到更好的,不是吗?而至于是否适合自己这个问题,就只有自己才能感受了,其他人的回答只是参考。我相信题主已经做出了选择,等他过段时间再来更新吧。

ThoughtWorks是一家奇特的公司,奇特到她的员工很少有说她坏话的,不管是在职还是离职;奇特到也有一些“ThoughtWorks终身黑”一直致力于各种嘲讽。当然,对于大多数人来说,她是神秘的。

1-mystery

因为她所倡导宣扬的东西,在其他公司尤其是传统IT企业来说都太难以推进了。比如Clean Code、比如TDD、比如持续交付(当然,如果你认为这些实践对于软件开发来说无足轻重,欢迎来辩)。因此曾经有不少技术圈网红希望能够到ThoughtWorks参观,无非就是想看看,“你们说你们XXX做得很好,到底是不是真的啊。”

我在加入ThoughtWorks之前也抱有这样的疑问。因为当我看着上千行的方法发呆时,他们说超过15行甚至5行就算大方法;当我们还在没有单元测试的深渊中挣扎时,他们已经TDD得不亦乐乎;当我刚开始用Jenkins搭建好构建环境,《持续交付》出版了;当我们终于搭建起分布式应用,觉得差不多算是SOA了,人家已经把大服务砍成微服务了!同样是程序员,这么一比怎么就感觉自己不会写程序了呢?他们简直就是神一般的存在啊。

2-hero

到底是不是真的呢?以我有限的ThoughtWorks项目经验来回答:“并不全是真的。”超过15行的方法比比皆是;TDD倒是一直在践行,但是团队成员对它的质疑从来就没有停止过;持续交付还在路上,还做不到每天都部署到生产环境,我们相比Amazon还差着好几千个Facebook;而一度客户对我们的challenge是,“别再加服务了!”

但是,与其他企业不同的是,ThoughtWorks一直把这些优秀实践作为追求的目标。前路也许坎坷,但永远不会放弃。对于“代码坏味道”的争执可能贯穿整个Code Review,不辩个淋漓尽致不痛快,不为别的,只为把代码写好;TDD虽然有争议,但都在努力学习,各种workshop和培训纷至沓来,不为别的,只为get一门新技能,融会贯通突破极限成为大神;企业应用的持续交付难度比互联网要大得多,但这条路我们也基本上趟出来了,并且会坚持走下去;至于微服务,经过了那段低潮期,我们从各个方面开始调整,已经基本找到了应对之道

3-solution

所以,重要的不在于这些东西我们现在做得好不好,而在于我们一直在往好的方向上做。而在这个过程中,收获最多的不就是人的成长吗?

回到主题上来,我当初为什么选择ThoughtWorks呢?坦白地讲,对于一个在国企工作多年饱尝了世间冷暖、见识过项目百态技术却没啥进步的我来说,在换工作时并没有太多可选择的余地。要不是在拿到ThoughtWorks offer的两周前我的人生导师推荐我去ThoughtWorks试试,我可能就去一家房地产企业做甲方的技术经理了。

但选择公司这一行为不仅仅发生在当初挑offer时,也发生在每年的离职季。每年春天,拿完年终奖、过完年,那些对promotion不甚满意的年轻人就开始躁动起来,人员流动如滔滔江水连绵不绝。对于每个人来说,是去是留都要做个了断。那么我为什么每次都选择ThoughtWorks呢?

4-insights

14年初,我刚刚过试用期,对ThoughtWorks的一切都还处于看似懂了但现在看来其实too young的阶段。那时我留下来的理由非常简单,我还没有领悟到这家公司的精华,还需要不断地学习。

15年初,我所在的项目新启动了子项目,使用ReactJS作为前端框架,同时还引入了node.js。对于一心想学习JavaScript的我来说简直如沐春风。在这个时间点,国内很多公司都还没有引入ReactJS。顺便说一句,该项目从10年便引入了AngularJS,同样是国内领先。对于新技术孜孜不断地追求把我牢牢地拴住了。

时间来到去年底今年初,一位同事找到了包括我在内的几个人,成立了一个“地下组织”,开始互助写作。所谓“互助”其实就是互相督促、互相建议、互相伤害而已。除我之外,其他人都是公司里举足轻重的人物,不知道这位同事为啥会找到默默无闻的我。在其他公司,这种事发生的概率恐怕会很小。谁管你有什么特长喜欢什么呢?

5-who-cares

而在ThoughtWorks,这样的地下组织有很多。志同道合的同事们会自发地聚集在一起,为做成点事情而努力。比如BQConf和CDConf,都是大家觉得想做点什么,于是找到了一拨人一起做,需要公司帮助的时候再找领导谈。没有一个是自上而下的“委派”。半年来我们这个组织诞生了不少优秀文字、演讲和播客,以及最新的ThoughtWorks读书雷达

三个月前,我们这个小组织进行了一次团建,期间问到是什么动力促使自己坚持下来。一些同事的回答让我动容。他们的大意是说,“希望通过自己的微薄之力,保持ThoughtWorks在社区的影响力,不能让ThoughtWorks的招牌砸在我们这一代ThoughtWorker的手中。”一个普通的员工对自己的公司爱得如此深沉,让我肃然起敬。不知道文章开头和ThoughtWorks比较的那家大公司有没有员工愿意无偿做这些事情,反正我是真的为能和这样的人成为同事而感到骄傲。

行文至此,已是深夜。心有戚戚,无以言表。思绪万千,不知所云。

以上,献给入职三周年的我。

Share

微服务概述

“微服务”这个术语在过去几年如雨后春笋般涌现,它是一种构建可独立部署服务套件的软件设计方式。虽然这样的架构风格没有明确的定义,但它们在组织方式、业务能力、自动化部署、智能化终端以及对语言与数据的去中心化等方面具备共同的特征。

以下内容摘自Martin Fowler的网站

“微服务”,又一个出现在拥挤的软件架构街道的新名词。虽然我们的第一反应是不屑一顾,但它的确是一个出镜率越来越高的软件设计风格。在过去的几年中,我们已经看到很多的项目使用了微服务,目前来看效果不错,我们很多同事已经将它作为构建企业应用的默认方式。但很遗憾,并没有很多资料解释微服务是什么以及如何实现微服务。

简而言之,微服务是一种将单个应用以许多微小的服务所组成的服务套件的形式来构建软件的方法,每个微服务拥有自己的轻量级数据处理模块以及通信机制(通常是HTTP API的形式)。微服务围绕业务能力和各自独立的自动化部署机制构建而来。由于微服务需要极少的集中管理,因此各个服务可以使用不同的编程语言以及不同的存储技术。

为了解释微服务的设计风格,我们先来把它和单块架构风格做一个比较。单块架构的应用只有一个单元。企业应用常常包含三个主要部分:客户端用户界面(包括运行在用户计算机浏览器中的 HTML 页面和 JavaScript)、数据库(包括保存在常见的关系型数据库中的各种表)和服务器端应用程序。服务器端应用程序处理 HTTP 请求,执行业务逻辑,在数据库中检索和更新数据并选择和渲染 HTML 视图发送到浏览器。此服务器端应用程序是一个完整的、 单一的逻辑可执行单元。任何对系统的更改都需要构建和部署完整的服务器端应用程序的新版本。

这样的单块服务器是构建系统最自然的方式。所有处理请求的业务逻辑都在同一个进程中,它允许你使用编程语言的特性来将整个应用划分为类、函数及命名空间。利用某些方法,你可以在笔记本电脑中运行和测试应用程序,并使用部署流水线确保新的更改通过了测试,并部署到了生产环境中。最后你可以通过增加运行实例并进行负载均衡,对应用进行横向扩展。

单块应用是可行的,但越来越多的人在使用的过程中受挫,尤其是随着越来越多的应用被部署到云中。整个系统的更新周期是被绑在一起的——对应用的一小部分进行了更改,就需要整个系统重新构建和部署。随着时间的推移它往往很难保持一个良好的模块化结构,继而难以保证新的更改只影响其所在的模块。需要对应用进行扩展时,只能将整个应用一起进行扩展,而不是扩展应用中的某个部分,这也消耗了更多的资源。

MicroservicesFowler

这些挫折导致了微服务的架构方式:以服务套件的形式构建软件。微服务是独立部署的和可扩展的,每个服务都有明确的模块边界,甚至允许不同的服务使用不同的编程语言,它们甚至可以由不同的团队管理。

我们不认为微服务架构是什么创新,它的历史可以追溯到 Unix 年代的设计思想。但还没有足够多的人考虑过采用微服务架构,如果他们使用微服务,很多软件开发过程会变得更好。

了解更多信息:

詹姆斯和马丁的文章接着罗列了9个微服务的特点来定义什么是微服务架构,并探讨了其与面向服务的架构(SOA)的关系,最后论述了这种风格是否是企业软件的未来。

请到这里继续阅读:http://martinfowler.com/articles/microservices.html

James Lewis是 ThoughtWorks 的资深咨询师,也是技术顾问委员会的成员。James 的兴趣点在整合企业级系统时采用小型的相互协作的服务构建应用。他已经成功得构建了许多采用微服务的系统,并且在最近的几年里一直是社区的活跃分子。

Martin Fowler 是一位作家,演讲家和大名鼎鼎的软件开发者。如何组件化软件系统的问题一直困扰着他,一些模棱两可的答案并没有让他满意。他希望微服务不要辜负倡导者们所提出的承诺。

Share

[C#解惑] #1 在构造函数内调用虚方法

谜题

在C#中,用virtual关键字修饰的方法(属性、事件)称为虚方法(属性、事件),表示该方法可以由派生类重写(override)。虚方法是.NET中的重要概念,可以说在某种程度上,虚方法使得多态成为可能。

然而虚方法的使用却存在着很大学问,如果滥用的话势必对程序产生很大的负面影响。比如下面这个例子:

public class Puzzle
{
    public Puzzle()
    {
        Name = "Virtual member call in constructor";
        Solve();
    }

    public virtual string Name { get; set; }

    public virtual void Solve()
    {
    }
}

如果您的Visual Studio没有安装ReSharper,那么上面的代码不会有任何异常。但如果安装了,在构造函数内部给Name赋值和调用Solve时就会在下面产生一个波浪线,即警告:virtual member call in constructor。

8142-20160126002554738-552318815

这是什么原因呢?我们在构造函数中调用虚方法,碍着ReSharper什么事儿了?

其实这个警告就是提醒我们不要在非封闭类型的构造函数内调用虚方法或虚属性。但为什么这样做不合适呢?在解惑之前,我们先来了解两个概念。

类型的初始化顺序

我们先来看这样一段代码:

class Base
{
    public Base()
    {
        Console.WriteLine("Base constructor");
    }
}
class Derived : Base
{
    public Derived()
    {
        Console.WriteLine("Derived constructor");
    }
}
static class Program
{
    static void Main()
    {
        new Derived();
        Console.Read();
    }
}
`</pre>

猜一猜它的输出结果是什么?

你也许已经猜到了,它的结果是:
<pre>`Base constructor
Derived constructor
`</pre>

我们在初始化一个对象时,总是会**先执行基类的构造函数,然后再执行子类的构造函数**。

### 虚方法调用

我们再来看一段代码:
<pre>`class Base
{
    public void M()
    {
        Console.WriteLine("Base.M");
    }

    public virtual void V()
    {
        Console.WriteLine("Base.V");
    }
}
class Derived : Base
{
    public new void M()
    {
        Console.WriteLine("Derived.M");
    }

    public override void V()
    {
        Console.WriteLine("Derived.V");
    }
}
static class Program
{
    static void Main()
    {
        var d = new Derived();
        Base b = d;
        b.M();
        b.V();
        d.M();
        d.V();
        Console.Read();
    }
}
`</pre>

再来猜一猜输出结果吧。

貌似应该是:
<pre>`Base.M
Base.V
Derived.M
Derived.V
`</pre>

但运行一下会发现,真正的结果是这样的:
<pre>`Base.M
Derived.V
Derived.M
Derived.V
`</pre>

这是为什么呢?

原来对于非虚方法调用,编译器会进行一些额外的“动作”。比如**找出所调用对象的实际类型**,以访问正确的方法表(调用`b.V()`的时候就会找到变量`b`的实际类型`Derived`,从而输出`Derived.V`)。

### 解惑

现在回到我们最初的谜题,virtual member call in constructor。结合以上两个知识点,会有哪些发现?

我们稍微改造一下虚方法调用的那个例子。
<pre>`class Foo
{
    public Foo(string s)
    {
        Console.WriteLine(s);
    }
    public void Bar() { }
}

class Base
{
    public Base()
    {
        V(); // Virtual member call in constructor
    }
    public virtual void V()
    {
        Console.WriteLine("Base.V");
    }
}
class Derived : Base
{
    private Foo foo;
    public Derived()
    {
        foo = new Foo("foo in Derived");
    }

    public override void V()
    {
        Console.WriteLine("Derived.V");
        foo.Bar(); // will throw NullReferenceException
    }
}

Base的构造函数中调用虚方法V()时,ReSharper会给出virtual member call in constructor的警告。这是因为V可以在Base的任意子类中被改写(override),而这种改写,很有可能使得它依赖于自己的构造函数,如上例所示。而由于之前提到的类型初始化顺序,在执行Base b = new Derived();这样的代码时,Base的构造函数要早于Derived的构造函数执行,因此在执行到foo.Bar()foo还是个空引用。

明白了吗?我们来简单总结一下。Virtual member call in constructor的警告是因为,对于Base b = new Derived();这样的代码:

  1. 基类构造函数的执行要早于子类构造函数
  2. 基类构造函数中对于虚方法的调用,实际调用的是子类中重写的虚方法

因此,ReSharper会警告我们,这么做存在隐患。

我们能完全避免这么做吗?很遗憾,答案是不能。比如如果项目中使用了NHibernate,框架本身要求ORM实体类中,所有与数据库列具有对应关系的属性都必须为虚属性。这是因为NHibernate为了实现延迟加载,会为每个实体类生成proxy,这些proxy需要重写实体类中属性的getter/setter。而有些时候,为了业务需要,我们不得不在实体类的构造函数中对这些属性进行某些操作(比如初始化)。

我认为这么做是技术选型所致的必然结果,是完全可以接受的。但我们要注意,在代码中保证那些可能会被继承的实体,在子类中重写那些虚属性时,不要依赖于子类自身的构造函数(这几乎是可以保证的,因为与数据库列映射的属性,只能是最简单的getter/setter)。

Share