Notes

Dism.exe /Online /Cleanup-Image /AnalyzeComponentStore
Dism.exe /online /Cleanup-Image /StartComponentCleanup

优酷(土豆)帐号:13420731947密码:8417457

豆瓣真傻比

一个月前,给talebook.org 添加了社会化用户登录功能,使用的是python-social-auth库,搞定QQ、微博和豆瓣三种登录方式。

今天兴起想分析下咱这个站点有多少个用户了:QQ有16个,Weibo有15个,Douban有0个!

好平均的样子嘛……咦?怎么完全没有豆瓣的用户??爱读书的文艺青年们呢?

人肉测试了一下,发现竟然是因为豆瓣登录失败了!错误提示如下:

HTTPError: 403 Client Error: Forbidden for url: https://www.douban.com/service/auth2/token

不信邪,人肉拿curl来访问,竟然成功了!再次转入python-requests库,却又失败了!

机智如我,瞬间猜测这是触发了某种规则,被封禁了。十有八九是靠User-Agent来封禁的。顺手查了下python-social-auth的代码,发现贴心地准备着一个user_agent()函数!(只有reddit启用了该函数)

启用后,世界和谐了。

顺手在Q上向豆瓣的tony吐槽了一番。哼

EISCONN的故事

在这春风明媚的日子里,有位T同学很苦恼。忙碌了一整天,有个BUG愣是定位不出来。简单描述呢,现象是这样子的:

第一次处理是正常的,但是后续的处理就是报错。sendto()调用错误码是 EISCONN(已被连接)。

忧伤的问题

当然,代码BUG的范围也很快确定了,就是新加入的statsd-client-cpp工具库里。代码量不到两百行,失败的地方就是在sendto()的执行里(代码看这儿)。一看错误码(EISCONN,比较少见),说“socket已经被连接”——但咱这明明是UDP协议啊,无连接无connect的!

T同学咨询了周围的大师们,翻阅了《Unix网络编程》(没错,这书在上次的故事里也出场了!),里面说:

sendto()函数的执行流大约是这样子的:

  1. 连接套接字;
  2. 输出数据报文;
  3. 断开套接字连接;

无语了。按照圣经里说法,连接都是被断开了的啊!还怎么会报错“已被连接”?!!

失败的补救

T同学比较耿直,对付BUG比较直接粗暴:报啥错误就解决啥错误!思路有俩:

  1. 人肉断开它!
    这个实际上是行不通的(只能整个儿socket关闭销毁掉,不能只断开)

  2. 连接了也继续发!
    虽然一般情况下UDP协议的程序都是不管连接直接发sendto()的,但是先连接后再write()也是可行的。

于是按照方法2搞起!然而,实践证明这是不给力的,UDP对端根本没有收到任何数据!

正确的思路

这种头痛医头、瞎试几次的做法,本质上就不能算正确的方法。我们的思路应该是:

  1. 收集现象
  2. 分析原因
  3. 验证方案
  4. 解决问题

上面折腾了这么久,基本上还是只有一个错误码和代码出错的位置,现象数据太少了。T同学冷静下来后,开始祭出大杀器strace工具程序!
strace程序能够捕抓系统调用(system call),并把这些调用接口的时间、参数、返回值、耗时等等都记录下来;输出信息可读性相当的高(比起gdb爽多了),能反映出系统最底层的运行状况,是后台开发程序员的居家旅行必备的强(zhuang)大(bi)工具。

神奇的零

具体strace的过程就不多说了。调整进程数量、执行strace、查看log、研究执行状况:

strace32 -s 10086 -o /tmp/Strace.log -tt -p $(pidof -s get_api_key)

强大万能的strace

终于发现一个关键现象:每次的连接socket()返回值都是0!
这里稍微解释一下,为什么零值会是很特殊:socket()返回值表示的是文件描述符;在POSIX标准里,有三个特殊的文件描述符值0、1、2,分别是STDIN(标准输入)、STDOUT(标准输出)、STDERR(标准错误)。所以默认情况下,零值都会作为STDIN(标准输入)使用;只有当程序主动关闭了STDIN时,系统才会分配0值给socket()使用。

所以这时候思路有两个了:

  1. 假如系统分配的0值是正确的,那么得寻找0值后来被“弄坏”(已连接)的出现地方;
  2. 假如系统分配的0值是错误的,那么一定是某处BUG“失误”把STDIN释放掉了;

这种“辩证”的思路,让我忽然想起了《撸撸姐的超本格事件簿》里的给出各种伪解答搞笑助手:每次破案分析都至少有正反两个思路,看起来毫无盲点,但总是被撸撸姐指出第三种情况!哈哈哈!!!不过在现实场景里,正反两面都深入思考的做法,一般帮助很大的。

谁弄坏了0

所以0是被谁弄坏的?怎么弄坏的?

为了深入探究这个问题,得先了解一下程序运行的环境。这个工具库之前已经在现网的服务器里跑了很久,也有两个简约的单元测试。都没有发现过问题。这次是在往CGI接口里使用,运行环境是QZHTTP+FastCGI。

所以正常情况下,STDIN应该是CGI与qzhttp收包程序之间的连接,用来传递HTTP请求报文。而分析到的一个现象是,CGI程序的功能完全正常!也就是说程序之间传递数据(STDIN)是正常的……等等,STDIN(0值)不是分配给我们用吗??为毛还不影响功能啊啊啊???

秘诀就是——dup2(a, b)!!这个函数能够把一个文件描述符的信息拷贝到另一个描述符里!所以文章最初出现的“EISCONN”错误的原因是:

原本UDP无连接的socket,被人用dup2()“篡改”了,于是就变成了另外的文件描述符,出现了“已连接”的状态。

写个小程序验证了一下,果然能够随便拷贝到STDIN描述符里!!!而再次strace抓包查看,也发现了明显的dup调用:

dup2调用证据

可以清晰看到对STDIN、STDOUT都做了dup2()操作!!!太邪恶了!!!火速在万能的StackOverflow.com上也找到了一篇关于0值的问答,里面有人提到了如何解决这个问题!那就是把0、1、2预留下来给系统!老子不陪你们玩零了!!(傲娇地离去!)

另一个思路

等等,刚才怎么这么快就进入了“解决问题”的节奏??? T同学的案例虽然因为屏幕不够长被滚动掉了,但是我们要分析所有的现象找到原因啊!

于是进入刚才的第二个思路:是谁把STDIN给关闭了??

STDIN被close()的证据

深入strace的log,发现close(0)首次出现的位置没有太特别,距离后来socket()和sendto()调用很遥远,暂时发现不了什么(后来细想,考虑时序其实是个误导)。

不过,因为是新引入的库导致了这个问题,基本上猜测要么就是库代码有BUG,要么是库和QZHTTP不兼容。重新阅读代码中与close()有关的片段,尝试梳理一下思路.果然发现一个问题:

代码中的close()

d->sock是个关键的值,而且没有初始化!一般来说变量没有初始化,会是一个随机的值;但是我们的场景里,StatsdClient对象是一个单体实例,以static限定符实现的——所以是有初始值0的——刚好触发了BUG。

解决

再次核对代码逻辑和strace的log,脑补了一下执行的流程,基本上就确认BUG的原因就是这里了。

所以啊,在构造函数里增加一个初始化 d->sock = -1; 问题解决了。

写到这里的时候,忽然想起上两周给赵总用这个工具库,他也出现了一些诡异问题。当时他的现象是:随机出现accept错误。现在看来BUG原因都是一个,只不过赵总的使用方式是临时变量,没初始化的值就是随机值,因此偶然触发故障(他当时引起的是accept失败,也是描述符问题)。而这次是必现的问题,定位起来轻松一些。

思考

所以,要保持良好的代码编写习惯。顺手初始化了,就不会有这么纠结的问题了。(T同学浪费了一个春风明媚的周五,我错过了一个骑车回家的清凉傍晚。)

另外,厚脸皮承认,这个库是我写的,BUG也是我弄的。

POLLERR的故事

今天code review时,同事B对我代码中的poll()的处理做法提出了异议。于是做了些研究,还发现了一些好玩的故事。

异议的代码

我的代码是参考manpage写的,类似下面的做法。同事B说没有处理POLLERR、而且应当使用else if

OK。我赞同补充POLLERR的处理,但不赞同使用else if。原因:

  • fd的读事件、写事件可能会同时到达,因此我想同时处理这两个事件;
  • Linux Manpage里面的示例,就是三个if语句独立的。
ret = poll(fds, 2, timeout_msecs);
if (ret > 0) {
    /* An event on one of the fds has occurred. */
    for (i=0; i<2; i++) {
        if (fds[i].revents & POLLIN ) {
            /* Priority data may be written on device number i. */
            ...
        }
        if (fds[i].revents & POLLOUT ) {
            /* Data may be written on device number i. */
            ...
        }
    }
}

诡异的经历

但是同事B举出了他偶然体验到的诡异经历:

POLLIN, POLLOUT, POLLERR同时出现。

在这种异常下,我的代码处理逻辑就会坑爹了。

于是问题变成了,什么情况下会出现这种诡异场景、三个事件同时出现究竟是什么含义?

翻阅《UNIX环境高级编程》、《UNIX网络编程》里面对poll()的讲解,均没有提到信号是否会同时出现的问题(所以也没提到该不该用else if的事情了)。

在Github上查找POLLERR相关的代码,发现大多数人都是用3个if语句处理这三个事件。那真相究竟是啥?

牛人的解答

百般搜索,终于在StackOverflow.com上看到有人提到了一个相似的问题:

Sometimes epoll_wait returns with both POLLOUT & POLLERR events set for the same socket descriptor.

终于下面有大神做了解答

Here is some good information on non-blocking tcp connect().

When a socket error is detected (i.e. connection closed/refused/timedout), epoll will return the registered interest events POLLIN/POLLOUT with POLLERR. So epoll_wait() will return POLLOUT|POLLERR if you registered POLLOUT, or POLLIN|POLLOUT|POLLERR if POLLIN|POLLOUT was registered.

Just because epoll returns POLLIN doesn't mean there will be data available to read, since recv() may just return the error from the non-blocking connect() call. I think epoll returns all the registered events with POLLERR to make sure the program calls send()/recv()/etc.. and gets the socket error. Some programs never check for POLLERR/POLLHUP and only catch socket errors on the next send()/recv() call.

翻译一下:

这儿有些很赞的关于非阻塞TCP connect()的信息。

当一个socket出现错误时(例如 连接断开/拒绝/超时),epoll()会返回POLLERR加上注册时的POLLIN/POLLOUT事件。所以,如果监听的是POLLOUT,那epoll_wait()会返回POLLOUT|POLLERR;如果监听的是POLLIN,那epoll_wait()会返回POLLIN|POLLERR。

注意epoll()返回POLLIN并不表示会有数据可读,因为recv()会立刻返回前一个错误码(即非阻塞的connect()调用)。我个人认为epoll()返回所有的注册事件加POLLERR,是为了确保程序会调用send()/recv()等等,进而发现socket出错了。毕竟有些代码从来不检测POLLERR/POLLHUP,只折腾send()/recv()等函数的错误码。

呵呵,Github上翻看了这么多代码,的确是大神说的样子。

验证

所以同事B的经历是常见的场景。而且很容易就能够触发。只要在连接上闹些问题,就能达到目的了。例如下面这段代码演示了连接失败时,POLLERR/POLLIN/POLLOUT事件都同时触发了。

示例中使用了getsockopt()来获取错误码;也可以直接使用read()/write()也是能够获取相同的错误码。

深入探究

StackOverflow的大神只做了简要的解答。真正的原因只能自己去翻看代码了。

翻阅内核代码(我的系统版本是Linux-2.6.32.57-x86 ),可以看到在tcp_poll()里(net/ipv4/tcp.c的389行,我的场景是TCP),对于所有sock错误都置了POLLERR。而异常情况下,POLLIN/POLLOUT则分别与RCV_SHUTDOWN/SEND_SHUTDOWN有关。换个视角,和连接断开有关的代码在tcp_reset()中(net/ipv4/tcp_input.c的3957行)的处理,里面的tcp_done()代码)则明确设置了sk->sk_shutdown = SHUTDOWN_MASK——所以,对于关闭的连接,总是会有POLLIN/POLLOUT事件!

研究到此解决。真相大白。

所以啊,我还是听取同事B的建议,加个else if优化一下处理逻辑吧。

面试官经验谈

去年十月的时候,团队急需人才壮大实力,于是主管安排我参加到面试流程里,作为初试官。职责是第一时间处理招聘组同事推送过来的简历,打电话面试,考核技术能力和其他软实力。面试了几十人,最终才招到了一个新同学。前几天,主管又给我几份安卓开发岗位的简历,让我做个初步面试。虽然技术道路有些不同,但是凭着一些通用的面试官技巧,还是对付下来了。

面试很重要啊

当我们面试时,我们面试的是什么

面试时关注的内容,主要是技术能力和软实力。技术能力的要求根据岗位不同,会有所区别:例如后台程序开发,会关注高性能服务器设计、操作系统原理、网络通信原理、大数据处理等;又例如安卓开发则关注编程能力、网络通信知识、移动设备开发经验等。软实力则包括表达沟通能力、学习能力、抗压能力、团队能力、性格特点等。

那么我拿到简历时,会怎么做呢?

初试官的面试流程

  1. 看项目经验。需要招聘的是C++后台程序开发,所以过去两年项目经验不匹配C++、后台服务的,全部都过滤掉。例如嵌入式的开发、驱动程序的开发、J2EE的开发都不行。游戏开发和应用开发两者略微有差异,具体面试时也会稍加考虑。

  2. 看项目难度。尤其关注用户量很高、数据量很大的项目,它们对候选者的能力挑战和锻炼都是很明显的。(例如Flexcoin被盗比特币的事情,就是高并发时的问题。)如果是一些非直接面向普通用户的产品,那就会关注候选者所涉及的技术深度是否足够——通常接触底层核心的候选者会更受青睐。

  3. 聊项目过程。大部分情况下,面试官作为一个倾听者的角色,引导候选者讲述项目的研发过程。主要是关注团队规模、工作职责、系统难点与解决思路、以及技术上的细节(判断研发真实性)。项目虽然各异,但是基本框架是一样的:业务逻辑、数据存储、性能与缓存、网路交互、运维性和扩展性、数据与统计。一般要求能够举出各个模块下的常见做法、并且根据业务场景分析利弊,同时还要能够回答出行业在该问题上的最新动态。对于大型系统,运维性和扩展性是很重要的,数据与统计则更是高级工程师必备素养。因为这两项要求已经跳出了纯粹开发的范围,思维更广了。能在这两点上回答好,能加分的。

  4. 聊项目难点。通常是直接问候选者“项目中最有收获的是哪些方面?” 这里有话术技巧,本身问得很虚(不仅限于开发技术细节),候选者的回答可能是业务上手熟练度(某技术没有接触过,花了若干时间学会了)、可能是开发难点挑战(某功能原本是A做法,实践过程发现B做法更优)、可能是其他杂七杂八的答案。从候选者的回答中,能够看出他对困难点的理解、对未来挑战的承受程度、以及思考总结能力。

  5. 测试软能力。前面的面试都是聊过去发生的事情,核心在于让候选者说清楚说全面,同时面试官也要评估是否真实无水分。软能力则是聊假设的问题,通常会在面试末期设置疑难场景进行测试、也会穿插在前面聊项目的过程中进行。尽量让提问变得自然不刻意,目的是让候选者展示解决问题的思路,以及体验出来的软能力。

软能力也是关键

软能力测试需要使用一些话术。分享我常用的一些套路:

  • 压力测试:例如候选者可能会提到项目中某个技术指标做到了2万每天,那么可以顺势提问:“2万每天你个人觉得是高还是低?如果说业界有人做到200万每天,你认为该怎么做?”前半句能考验候选者对系统的数据与瓶颈的掌握程度(体现着深度、自信、尽责),后半句则设置了一个百倍的压力(尤其是说业界“已经做到的”了),考验候选者的架构能力、技术专业程度、思维能力。
  • 新领域测试:例如跟候选者暖场时可能会聊起朋友圈、比特币火爆等话题,那么可以顺势提问:“朋友圈功能你也经常使用的,你觉得它的架构实现是怎么样的?” 冷不防地考察候选者对未知领域的问题的处理能力。候选者如果技术功力深厚,能够很快地抛出一个简单的架构设计的(因为本质上很多系统架构是类似的);我们重点关注候选者如何识别系统核心功能、以什么思路完成这些功能、预判系统最大挑战、如何解决这些挑战。
  • 团队能力:例如候选者提到团队有若干人时,顺势提问:“团队分工是怎么样的,你觉得合理吗?如果某人的技术意见跟你相反改怎么办?(例如不想要你实现的框架)” 大部分候选者回答,会以实际结果来评判技术分歧;但是应到要注意到要考虑项目时间预期、流程的制定、新人的学习教导等话题。
  • 表达与思路:这个完全穿插在整个面试里了。好的回答是要有123的顺序、并且是按某些因素分类的、递进式的思考;如果是飘散式的回答(问题可以A做法,B做法也能解决一部分,偶尔我们也用C做法),则体验出候选者思维能力不够强,对工作缺少总结。
  • 产品能力:互联网行业最讲究这个。一般会让候选者聊聊产品发展的想法,体现出“视角不局限于一亩三分地”的优点,有些加分的作用。(但不能说得太多,否则就不是技术面试了)

总结

电话面试大约是20~30分钟。总结起来就是“真实地表述项目经历+自信地处理问题挑战”。这里聊得都是初试官的面试重点,性格测试、职位工作和薪金等,都是后续的技术复试、老板面、HR面关注的了。初始官一般也能考察出一些性格特点,但不会特别关注(例如死心眼专研技术的沉闷、泛泛而谈的轻浮、就事做事的安稳等)。

对比起来,最重要的是软能力的测试——即使是跨行业地去面试,也能很快考察出候选者是否专业、靠谱。