关于公司架构的一些吐槽

写了这么多文章,技术屌丝味太浓了,以前每次想写点随想也是起了个标题就没下文了,正好今天早上看到了一篇喷技术创业的文章,文中观点不敢苟同,趁着早晨清醒的时刻,咱也文艺范一次,写点有感而发的文字。

很多公司,无论大小,都想将部门划分的细之又细。所谓麻雀虽小,五脏俱全。随意拉一张公司的组织结构图出来看看 – 总裁办,行政部,人力资源部,技术部,产品部,市场部,客服部,运营部。。。别人有的我们就得有,别人没有的我们也得有,不论有些部门是不是必须,只要一两个人也能成立个部门。不明真相的人跑来一看,瞧,多正规,高端洋气上档次。

且不论其他职能部门,我就来说说跟研发关系最大的一些部门,以及如何显得他们的工作是不可或缺的。

产品部

产品部本身就是一个比较扯的部门,因为这个词打击面太大,整天挖空心思想点子的,想的是不是产品?没日没夜扣腚的,做的是不是产品?上线后胆战心惊监测的,维护的是不是产品?我们尊崇’less is more’,谁也说不清产品到底是个啥玩意,但是就有这样一个部门,叫做“产品部”。

再回到这篇文章的观点,技术人员过于注重技术,而忽略了对产品的思考,所以需要专门设置这样一个产品经理的职位来构思整个产品的走向。看似有理,但是他的前提是“技术人员不会思考产品”,这就是一个弥天大谎,我见过的大部分技术人员,对于项目的细节,业务逻辑的理解都比普通产品高出不知道多少个档次,我们不能纸上谈兵,只有真正理解了产品实现的业务逻辑,才能提出更有建设性和创新性的意见,这叫站在巨人的肩膀上。但是我看到的很多产品是,进入公司一个礼拜都不到,以前从来没有用过公司的产品,就能洋洋洒洒写出一大篇设计文档来,而问到为什么要这样改,则又支支吾吾,说不出个所以然,最后来一句我看到某某网站也是这样做的,所以我们也要这样做。

回到技术人员的自我修养上来,我认为每个研发应该视自己开发的产品如孩子一般,在关注技术进步之余,能花时间来培养这个孩子,让其更健壮,更优雅,更招人待见。同时,也要多看看外面的世界,试用别人的产品,包括竞争对手的,以和自己创造的孩子进行对比。技术人员提出的产品改进意见往往更务实,更切中要害。

将这种视如己出的概念再推广到所有员工身上,其实人人都可以为产品添砖加瓦出谋划策,那么,产品部门存在的价值何在?

测试部

测试可以分为好多类:1.做黑盒的。2.做白盒的。3.自动化测试。 我们一一道来。

先说第一种,做黑盒的。很多公司曲解了黑盒的意思,认为做黑盒就是一个功能开发好了,你拿去用,不出问题就行了。这样就把软件开发变成了一个劳动密集型行业,这样的测试与拿到内测账号的用户有什么区别。更何况我们有些测试人员的使用经验可能还不如用户。这样的测试,效率既低下,效果还不明显。

再说第二种,做白盒的。一些公司有白盒测试,其实就是看看代码。很多公司认为,这个人写代码不行,就让他做测试吧。这是一个大大的误区,对测试人员的要求应该比研发更高,因为我认为读代码往往比写代码更难。上面这种做法的后果就是,一些人做白盒测试的时候,一旦看到不明白的地方,就跑来问研发,需要研发手把手的一条条教来,这不光是浪费双方的时间,也是在做无用功。我相信任意一个合格的研发人员,在提测之前应该都对代码做完整细致的自测。

最后说说第三种,做自动化测试的。这个其实要求较高,很多公司没有,即使有也是认为装个hudson写写配置文件就算自动化测试了。而我认为的自动化测试应该有很强的编码能力,能写出简单有效的单元测试用例,能配置不同的运行环境来确保软件的跨平台性,能进行性能测试,来保证软件的工作效率。这都对测试人员提出了较高的要求,这种人少之又少,而我见到的大多数测试人员连apache的vhost文件都不会写。

真正的测试人员应该得到相当的尊敬,因为他们需要对业务和代码都有很深的理解,他们比程序员更细心,既能防止程序员犯一些低级的错误,又能在一些容易忽略的问题是上保持警惕。

假如找不到这样合适的测试人员,测试部就没有存在的意义。

UED部

UED这个词是个舶来品,你甚至很难找到合适的中文替代词,只能用“用户体验”这种虚无缥缈的词来囊括。很多公司的UED,其实就是美工。

我自认为对设计和绘画不在行,所以我很尊重设计师的工作。设计师负责原型和效果设计,程序员负责实现,这是由人的左右半脑分工决定的,而且一个好的设计师,简而美的设计,应该是先在公司内部群体中产生认同感,才能让更多的用户认可。

下面是我的吐槽,一些我见过的设计师,可能读过一本Don’t Make Me Think,就能自诩得到了设计的精髓,接着就对页面开始做大刀阔斧的改革,也不需要其他人的意见,因为设计师都有一些自负天才的心理在作怪(嗯,其实程序员也有)。更有甚者,是一天一小改,三天一大改,也没有统一的风格,去别的网站东边抄一块,西边抄一块,就变成了咱家的东西。别人做瀑布流,我们也搞瀑布流,别人做下拉刷新,我们也用下拉刷新,可是从来没有独创的功能被人认可,这也是“一直在模仿,从未被超越”的典型了。

再说到“用户体验”这个词上,世界上恐怕再也找不到这样一个百搭的词了。一个按钮加大点可以叫用户体验,换个字体也叫用户体验,但是众口难调,人的大脑是很奇怪的,你喜欢的未必是人家喜欢的。所以真正需要做到的是在公司这个小范围内赢得众人的认同,然后将风格延续下去,这样才能让更多的用户接受乃至迷恋这种风格。有个很成功的例子就是苹果。

这里又要说到关于设计师和程序员两类人的区别了。我认为一个好的设计师和一个好的程序员应该是可以互补并相互学习的。我以前的一个研发经理,技术上肯定是毋庸置疑的,但是也做得一手好画,从鼠绘到原型到效果图,无所不能。这样的人才非常难得,但并不是没有,一个诸葛亮是远远高于三个臭皮匠的。而更多的人可以在学习中达到这个高度。

理想中的组织架构

任何组织发展壮大之后,人员就会变得复杂,管理难度相应也会增大。职场如战场,公司就是一个缩小的社会,我们不妨就从历史中找个例子出来。清廷倒台之前,曾有一番关于“共和”还是“立宪”的争论,两派人争得不可开交,最后的结果我们知道,共和派取得了胜利。

“共和”和“立宪”的根本区别还是权力集中在何处。相比帝制的独裁,“立宪”在名义上是说君主也要遵纪守法,权力还是集中于小部分人(真正的“立宪”君主只是个象征,内阁还是要选举的)。而“共和”则将权力下放于大众,政府则是大众意志的集中。

开源软件的圣经《大教堂与市集》其实也表达了这样一种思想。软件开发由自上而下的瀑布流发展到今天的开源模式,最有代表性的就是github flow。就是任何人都有权利对产品提出改进意见,并且可以亲自来实现这些功能,而项目的管理者(owner)在更新自己的产品之余,还需要接受来自四面八方的(pull request)。owner有权力决定这些功能的加入,也有义务鉴定这个功能的风险。

同样对公司的管理也是这样,权力不应该集中于个人,每个员工都应该有发言权。每个员工都应该有自由发挥的空间,来实现自己需要的功能。同时也不能忽略了合作的重要性,所以在日常的工作中,需要三两人一组,作为一个cell,可以是同一个部门中志同道合的人,也可以是不同部门中可以互补的人(比如设计师和程序员)。以cell为最小单位,来完成每个任务。最基本的目标:追求效率,追求自我提高,追求快乐工作。

这对个人也是有要求的,就是每个人都需要能独当一面。我想谁都不会愿意与一个一无所知和整日拖沓的同事合作吧(MM除外)。同时,管理层(owner)的责任就是协调cell之间的工作,有权力决定cell提出的功能的去留,也有义务听取大家的意见。

这也是我觉得创业公司比大公司好的一个原因,就是你有决定权,或者能将意见直接反馈到管理层,而不需要经过大公司死板结构的层层汇报。同样,这样的办事效率也是很多公司望尘莫及的。赠人玫瑰手有余香,以开放的心态接受他人,那么他人也会以同样的姿态来回报你。

最后贴一个豆瓣上的小段子,娱乐一下。不同部门员工吃饭时聊些什么

Javascript并行计算: Web Worker

最近发现了chrome下面的一个奇特现象,像下面这样的一段代码:

1
2
3
setInterval(function() {
  document.title = document.title.substr(1) + document.title.substr(0, 1);
}, 300);

这段代码本来是为了让标签栏内容出现滚动的效果,每300毫秒变化一次,这本来没什么问题,但是偶然切换的其他标签时,这个滚动的速度就会变慢,网上查了一下,原来chrome设计如此,当标签页不活动时,chrome会将所有定时任务的最小间隔设置为1秒,这样来减轻浏览器的压力,会影响所有带有timer的方法,如setIntervalsetTimeout。像上面这样的任务,间隔就被提高到了1秒。

由此引发的思考是,假如这个任务实时性要求很高,不容许这种时间机器的出现怎么办。stackoverflow也有人给出了一种解答,不使用内置的timer,而是在代码中主动计算时间差,来模拟setInterval的行为。这种方法能解决问题,但是总觉得不够“优雅”。更好的方法是使用html5的web worker

web worker目前支持的浏览器包括Firefox 3.5+,Chrome和Safari 4+。你用IE6?那自求多福吧。

搞过消息队列和异步计算的人对worker这个词应该不陌生,html5为我们提供了web worker这样一个优秀的特性,旨在将后台任务和前台交互分开,worker中的任务不会阻塞页面事件。我们先来解决上面提出的问题。

由于不隶属于任意页面,所以chrome不会将worker中的进程timer也改成1秒。所以我们可以对上面的代码稍作修改,拆分成worker和main两部分。

main.js
1
2
3
4
var worker = new Worker('worker.js');
worker.addEventListener('message', function(e) {
    document.title = document.title.substr(1) + document.title.substr(0, 1);
});
worker.js
1
2
3
setInterval(function() {
    self.postMessage();
}, 300);

这样触发更新title的任务就由worker来完成了。

上面只是一个粗浅的demo,worker真正的意义应该还是在并行计算,不过目前的web应用中前端基本没有大运算量的任务,所以worker在这里就没用武之地了。我们可以设想下面一种情况。

md5是很多网站用于保存密码的方式,由此也产生了很多md5解码的工具,由于md5是一种不可逆的加密算法,解密的方法除了使用字典以外,还有一种简单粗暴的方法,就是暴力破解,而这是非常耗时间的。我们拿到了一个加密过的字符串’77b3e6926e7295494dd3be91c6934899’,而且知道明文是一个六位的数字,那么可以用数字循环来制造碰撞(里面的md5方法是引入外部库,这里及不贴出来了):

main.js
1
2
3
4
5
6
7
8
9
var cipher = '77b3e6926e7295494dd3be91c6934899';
var start = new Date();
for(var i=0; i <= 999999; i++) {
  if (md5(i) === cipher) {
    console.log('plain text: ' + i);
    break;
  }
}
console.log('time cost: ' + (new Date() - start));

跑下来时间大概是12330毫秒。下面我们用十个worker来分担任务,实现相同的功能。

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var cipher = '77b3e6926e7295494dd3be91c6934899',
    workerList = [],
    start = new Date();
for(var i=0; i < 10; i++) {  // init 10 workers
  workerList.push(new Worker('worker.js'));
}
workerList.map(function(worker, index) {  // send task to each worker
  worker.addEventListener('message', function(e) {
    console.log('plain text: ' + e.data);
    workerList.map(function(_worker) {
      _worker.terminate();  // terminate all workers after task finished
    });
    console.log('time cost: ' + (new Date() - start));
  });
  worker.postMessage({
    start: index * 100000,
    end: index * 100000 + 99999,
    cipher: cipher
  });
});
worker.js
1
2
3
4
5
6
7
8
self.addEventListener('message', function(e) {
  for(var i = e.data.start; i <= e.data.end; i++) {
    if (md5(i) === e.data.cipher) {
      self.postMessage(i);
      break;
    }
  }
});

最佳期望是总时间的十分之一,实际执行下来用了2792毫秒,这与给worker分配任务的方式有关,假如我们给worker随机指派计算值,那么得到的结果会更平均,而不会因为密文的变化而有大的波动。

web worker对于javascript全局对象的访问也是有一些限制的,比如window,document,parent对象,这也是不能用worker取代所有页面script的一个原因。

关于worker的具体介绍,这篇文章讲的很好,里面还提供了几个现实的例子,非常详细。

参考文档

几款压力测试工具

压力测试工具林林总总,数不胜数,这里只列举几个命令行下常用的工具,来看看那种用的比较顺手,纯粹个人意见。

  • ab (apache benchmark,因为是apache自带的工具,所以用的人比较多)
  • siege (一个不错的开源压力测试工具,简单,好用)
  • httperf (据说很强大,但参数实在繁琐,不考虑)

ab

ab是apache自带的测试工具,很多情况下我们要测试一个网站在并发100下的响应速度,用下面的命令就行

1
ab -n1000 -c100 http://domain.com/

可不要少了最后的那个’/’,ab接受的url必须是schema, domain, path一个不少的。

其他像添加header,post参数等等的,man一下ab的手册吧。

长久以来我一直以ab作为webserver性能的一个指标,因为使用确实简单,直到遇见了siege。

siege

siege应该也有些年头了,2000年的时候就已经有这个软件了,直到最近更新了3.0.1版本。在第一次使用的时候,需要用siege.config在主目录下面生成一个配置文件.siegerc。这类文件常用linux的人应该很清楚了,里面每个选项都有明确的注释。需要注意的是delay这个值,做一般测试的时候会按照这个设定的时间间隔停顿,那么这样压力测试出来的结果就不准了,所以在做压测的时候需要加上’-b’参数,或者直接将这个参数设置为0。下面是一个100并发测试10秒钟的例子。

1
siege -b -c100 -t10S http://domain.com/

siege的一个缺点是没法设置总请求数,但是可以通过重复数和并发数组合来求出总请求数,例如我们要在100并发下发送1000个请求,就用下面的命令,当然这个时候就不能用’-t’参数了

1
siege -b -c100 -r10 http://domain.com/

结果与ab差不多,但是他会将历次测试的结果保存在一个文件中供以后比较。

测试

这里我分别用nodejs和php写了两个server脚本,响应值都是’hello world’,都用单一进程作为web server。(php5.4之后用-S参数来启动服务)。

测试结果:

1
2
3
(request/second)        php     nodejs
siege                   2500    2000
ab                      4880    3480

测试软件的评判标准不一样,所以测试结果有差距也比较合理,不能由此说明谁比谁更准确。但是在测试中nodejs的性能比php要慢20%这个倒是让我觉得比较惊讶,看来nodejs在运算效率上并不占优势。但是在复杂的web应用中,更多的时间开销在io中,nodejs合理的将这部分等待时间利用了起来,才会让人感觉比较快吧。

很早就有人对这几款测试软件做了横向的对比,有兴趣可以看一下,其中keepalive和no-keepalive的效率差距比较大,我自己测试时觉得并不明显,可能是写的例子比较单一吧。

今后假如需要做简单的压力测试,我还是比较倾向于使用siege。

Php 闭包:并不像看上去那么美好

最近一个叫laravel的php框架在社区讨论的风生水起,号称php界的rails,试用了一下,确实非常新鲜,但是又有种似曾相识的感觉。

例如路由中的一段代码:

1
2
3
4
5
6
<?php
Route::get('/', function()
{
    return View::make('hello');
});
?>

兄弟,你走错了,隔壁javascript出门左拐。

laravel号称将php5.3中新引入的闭包发扬光大,让代码变得更加灵活优雅,趣味十足。

在我看来,坑更多了。

为什么说php闭包没有看上去那么美好,因为他的生搬硬套。

闭包这个概念早已不新鲜,在函数是语言中被早已被用烂了,以至于现在lisp教徒抨击其他语言时都避而不谈closure和lambda,转而讨论currying,otp,metaprogramming等等更玄乎的东西。

php的闭包不能说引入的太晚,没有跟上编程发展的脚步,其实在php4时代,就已经有了这样的概念,call_user_funcarray_map等等方法都是支持callback方法的。但是时至今日,它依然是不完善的。

在发展的过程中,php引入了很多舶来品,例如接口,命名空间,异常控制等等,每种都是对自身语言已有编程风格的颠覆,以至于现在同样是编写php,不同的人能写出完全不同风格的代码。自然闭包也非原创,同样很怪异。

作用域

在javascript中,闭包内的变量是继承上层的,这是一种很自然的做法,也相当的灵活。但是php有自身的一套作用域规则,于是在闭包中使用变量就变得非常怪异,例如下面的例子:

1
2
3
4
5
6
7
8
9
10
<?php
$v = 1;
$arr = [1, 2, 3];
array_map(function($n) use ($v) {
    if ($n == $v) {
        echo 'exist';
    }
    return false;
}, $arr);
?>

这个use就是用来解决作用域的问题的,使用时可得瞧准咯,每个要用到的变量都得用use引入哦。然后当闭包身处类中时,情况又不一样了,下面的做法在php5.3中是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Demo {

    protected $val = 'v';

    public function getClosure() {
        return function() {
            return $this->val;
        };
    }
}

$d = new Demo();
echo call_user_func($d->getClosure()), "\n";
?>

因为php5.3不支持在闭包中使用$thisself关键字,但是在php5.4中得到了支持,所以上面的代码是可运行的,但是这让上面第一个例子情何以堪呢。

绑定

在5.4之后,php开始支持将一个闭包绑定到别的对象上,以便能直接调用这个对象的成员:

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
<?php

class Clo {

    public function __construct($val) {
        $this->val = $val;
    }

    function getClosure() {
        return function() {
            echo $this->val, "\n";
        };
    }
}

class Wrap {
    protected $val = 3;

    public function bar($foo) {
        call_user_func($foo);
    }
}

$a = new Clo(1);
$wrap = new Wrap();
call_user_func($a->getClosure());
call_user_func($a->getClosure()->bindTo($wrap, $wrap));
?>

这在某些场景下有用武之地,不过也要注意作用域,bindTo方法中的第二个参数是设置作用域的,向上面Wrap类中的变量$val是私有的,假如没有bindTo的第二个参数,是行不通滴。

Generator

顺便再提一下php5.5中新增的Generator,其中的yield支持运行时自定义方法,这显然又是从隔壁python借鉴来的,调用方式同样不是很自然,foreach承担了迭代的责任,相比于ruby中yield的强大功能,更是差之千里了。

不过有总比没有要好,将来一定有创意丰富的人能玩出更多花样,哦,貌似又多了一种编程风格。

[TDWTF] 我们相信傻瓜

一天早上,Stan去找他的上级Monty,他正在疯狂地的敲着键盘。这意味着两件事:明天他会收到一封讨厌的设计文档,而且那个设计中会用到一个从没用过的数据库。Monty是一个数据库“砖家”,没什么问题是他不能搞定的。Stan在这家公司的第一年,照做了Monty描述的每个设想,因为他不知道有哪些更好的办法。现在他对公司系统有了更多的了解,他渴望有个机会来做一些正确的事。

Stan在邮箱里找到了一份客户发给Monty的需求副本,要求两个ASP.NET应用之间能互相通信。Stan喜上心头,这是一个简单的web服务,既然他们用.NET,那么只要用WCF就行了。

Monty的介入绝对不可能玷污这个方案,他连.NET的基础都不懂。自从入职的第一天起,Stan就想着能对系统的设计有些话语权,这样才像一个“真正的开发”。最后,这个机会来了!

第二天,Monty的设计发到了Stan的邮箱。Stan心不在焉的打开它,就像一个验尸官掀开一张裹尸布。他翻开这份足足45页的“可扩展性数据库驱动进程内通讯框架”。这有点难理解,而且看起来有点像是重复造轮子。里面几乎没有提及客户的应用,因为它看起来想要将任意应用连接在一起。

这个设计需要用到11张数据表来传递元数据(发送,接收,时间戳,用户id,等等)和应用数据(统统被转成字符串并且储存在类似列1,列2等等的字段中)。当一个应用想要给另一个发送消息,它需要发送所有的会话/消息数据给一个存储过程。这个存储过程接收75个参数,大部分是可选的。另一个类似的存储过程允许发送者附加特殊的应用数据。而对于一个接收者,它需要调用SP_CHECK_FOR_MESSAGES_POLLING_PROCEDURE存储过程并传入它的PK_INT_APPLICATION_IDENTIFIER标识。当它消费完这条消息,还要调用SP_MESSAGE_TRANSACTION_COMPLETE_PROCEDURE存储过程来从“收件箱”中清除消息。Monty的系统会将这个事务中的所有数据移到一个结构相同的log表中,但是没有任何完整性可言。

在Stan砸碎屏幕之前,他听到Monty得意洋洋的说:“我对这事很兴奋,我希望这个能用在任何事上!”

“任何事?”Stan抑制住汹涌而来的恶心感。是时候让他坚持自己的原则,来表明他不再是那个毫无主见的职场新人了。“这个实现。。。很有趣,但是没必要用数据库来实现它”。

Monty一笑置之。

“.NET有个叫WCF的框架可以来帮助我们实现这个功能,”Stan继续说,“我们只需要写很少-”

“不行,”Monty不容置疑的说,虽然Stan知道假如他问Monty什么是“WCF”,Monty肯定会顾左右而言他。“我们在调试系统时会碰到一堆的问题。我们需要知道应用和应用之间是怎么通信的,谁,在什么时候,发送了消息。而且我们会将它储存在一个安全的地方。”

“但是,有一大堆的工具可以用来调试WC-”

“请实现我设计的系统。”Monty不留余地的走开了,以防Stan再有什么说辞。

Stan在与内心抗争中,花了几个礼拜的时间来实现这个冒牌的规范。错误不断的冒出来,而且没有什么好办法来解决强数据类型和同步性的问题。加入这种预防措施会让这庞然大物跑的更慢,虽然它已经够慢的了,而且仍然没法保证它按照预期工作。同时,Monty与客户的沟通不畅导致需求不断的变化。他的设计一天天的变化,最后成了一个64页的设计文档,需要14个数据表。

Stan受够了。他最后只能求助Monty的老板David。David实行开门迎客政策。Stan向David描述了现状。

“这不仅仅走了弯路,而且也不是客户想要的。用WCF的话我本来可以在几个礼拜前就完工,但是Monty不想这么干。”Stan总结到,“我觉得现在改正还为时不晚,但是Monty不赞成这样做。您能向他解释一下吗?”

David叹息道:“我知道了”。

终于!Stan兴奋地想他的建议成功了。

David停顿了一小会儿,然后像一个先知布道一样说,“你叫Stan是吧?Monty。。。有一些怪癖,有时候他会让你做一些毫无意义的事。我需要你继续下去并且相信一切都会好起来。他从公司成立时就在这儿了,我们的系统就像他的孩子一样,所以他知道哪种方式最合适。”

Stan明白现在最理智的是什么事情,他一言不发的回到自己的座位,经过一个小时的沉思,永远的离开了这个办公室。

后记

故事归故事,但很多公司的现状如此,如果不能在工作中提高自己,那么就想办法提高工作。后面的的评论也很有意思,可以看一下。

原文链接:In Fool We Trust

Start-stop-daemon

很多软件不提供init脚本,或者提供的脚本不合胃口,难免要自己动手丰衣足食。下面就推荐一个用来启动守护进程的神器。

start-stop-daemonOpenRC计划的一部分,这个程序最先出现在Debian系的Linux发行版中,这里有个比较古老的手册页面,更详细更直观的办法当然是通过man start-stop-daemon来查看手册了。我使用的是”start-stop-daemon (OpenRC) 0.10 (Funtoo Linux)”版本,大部分功能是差不多的。

start-stop-daemon最基本的两个功能就是--start--stop,简写为-S-K,然后再加上一个-s|--signal来给进程发送信号,功德圆满。

至于其中比较常用的一些参数,我列出来参考一下,以免忘了:

  • -x, --exec daemon,daemon就是真正要执行的进程脚本,比方说启动nginx,那么就是start-stop-daemon -x nginx
  • -p, --pidfile pidfile,指定pid文件,至于pid文件的用途就多了,stop,status都少不了它。
  • -n, --name,如果没有指定pid文件,那么就要通过指定name来停止进程了。
  • -u, --user user[:group],指定脚本用哪个用户或用户组执行,init脚本是必须使用root权限来执行的,但是它fork出来的子进程我们一般会选择一个权限较低的用户。
  • -b, --background,强制脚本在后台执行。
  • -m, --make-pidfile,这个一般和-b配合,用于生成pid文件
  • -d, --chdir path,切换进程的主目录,这个在构建守护进程的时候是很常用的。
  • -r, --chroot path,在某些安全性要求较高的情况下,我们就需要用到chroot将进程工作环境与物理环境完全隔离开来。
  • -1, --stdout logfile,将标准输出记录到log文件,与之相对应的就是-2, --stderr标准错误流。
  • -w, --wait milliseconds,进程启动后,有这个参数会等待几毫秒来检测进程是否仍然存活。

参数说完,下面就是一些需要注意的地方了。

-b与守护进程

-b是一个很常用的参数,我们使用start-stop-daemon的目的就是为了实现守护进程。但是有些程序自身也实现了守护进程的功能,比方说mongodb中有一个fork选项就是将自己在后台执行,这个时候假如搭配的-b参数,是得不到正确的pid的,因为start-stop-daemon只能得到最初启动的父进程pid,而父进程在fork完之后就自动退出了,那么start-stop-daemon就永远找不到正确的pid来结束进程了。所以使用-b的时候,一定要保证程序是在前台运行的。

其他参数

-x daemon后面跟的执行脚本必须只能是一个文件名,有些程序运行时还需要指定一些参数,比如nginx -c file来指定nginx的配置文件,使用start-stop-daemon -x "nginx -c file"是会报错的,这些程序内的参数以另一种方式加载,start-stop-daemon -x daemon -- $ARGV,这里的双横线--后面跟的所有参数就会被带到程序中了,比如start-stop-daemon -x nginx -c /etc/nginx.conf

下面是mongodb的一个init脚本,用start-stop-daemon是非常简单的。(貌似源代码中没有提供init脚本,只能自己动手了)。

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
#!/sbin/runscript
# Distributed under the terms of the GNU General Public License v2

MONGO_HOME=/usr/local/mongo
MONGO_USER=mongo
MONGO_PID_FILE=/var/run/mongo/mongo.pid

depend() {
    need net
}

start() {
    ebegin "Starting Mongodb"
    start-stop-daemon --start       \ 
        --chdir  "${MONGO_HOME}"    \ 
        --user "${MONGO_USER}"      \ 
        -m -p "${MONGO_PID_FILE}"   \ 
        -b --exec "${MONGO_HOME}/bin/mongod" -- --config=/etc/mongodb.conf
    eend $?
}

stop() {
    ebegin "Stopping Mongodb"
    start-stop-daemon --stop        \ 
        --chdir "${MONGO_HOME}"     \ 
        --user "${MONGO_USER}"      \ 
        -p "${MONGO_PID_FILE}"      \ 
    eend $?
}

使用 Coffeescript 的注意点

coffeescript是javascript的一个方言,随着javascript在前后端的流行,它在github的排名也扶摇直上,最近终于挤掉高帅富Objective-C跻身前十,可喜可贺。

虽然coffeescript号称”It’s just javascript”,但是相比较而言,仍然是添加了很多有趣的特性,大部分特性都是去粗取精,去伪存真,让js玩家喜闻乐见,让旁观路人不明觉厉,但是也随之带来了一些容易忽视的问题,不得不提一下,以免以后碰到后不知所措。

重载的符号

coffeescript重载了javascript中的一些符号和语法结构,最常用的就是==in

==

在js中最为人诟病的就是==符号表意不明,所以很多严谨的js开发者就强迫自己在比较时尽可能的使用===,coffeescript在这一点上做的更绝,你不能使用===,因为它将所有的==都转化成了===。这样对于一些经常需要在两种语言之间切换的码农来说,就是一种考验了。

in

在js中,遍历一个数组或hash对象可以使用for(var i in arr)的语言结构,这个时候遍历得到的i其实是数组的下标或者hash的key。coffeescript对in做了重载,使其更符合自然语义,遍历出的是数组的值和hash的value。同时引入of操作符,可以用它来代替原生的in,遍历出数组的下标,如for i of arr

class

原生的js中是没有class的概念的,但是有经验的码农会用prototype模型来将方法打包成class,以实现代码的重复利用。coffeescript中提供了class关键词,让类的实现和继承更加简单,但是也由此引发一些问题。假如说上面的问题只是人所共知的新特性的话,下面这些就是需要在编码时注意绕行的坑了。

变量名与类名

coffeescript对于类型和变量名并没有强制性的格式要求,这在其他语言中也不会出现问题,因为可以通过类型检查来区分两者,但是在coffeescript中,其实类和变量都是通过var关键词生成的变量,而在coffeescript语法中又禁用了var(这样就无法人为的指定变量的作用域,虽然coffeescript会比较智能的分配的作用域)。这在一般情况下也没有问题,直到碰到了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
class demo
  foo1: ->
    console.log demo
    return @
  foo2: ->
    demo = []
    return @

new demo().foo1().foo2().foo1()

==> [Function: demo]
==> []

同样的两次调用foo1方法,得到的结果却是不同的,这是因为foo2中的变量与类名冲突了,而且他们处于同一个作用域,这样foo2方法就变成了一个隐藏的地雷,踩到就爆炸。避免这种情况的一种做法是在命名上做区分,比如类命名必须以大字母开头,变量必须以小写字母开头,这样就不会造成这两者的混淆。

类成员变量

使用类的一个好处就是可以初始化一些变量,让这个类的所有方法共享,而又不会影响外层作用域。但是需要注意的是,javascript中对于数组和对象是引用传递,在coffeescript类中使用这两种类型作为成员变量时,就会产生一些不曾期待的后果。

1
2
3
4
5
6
7
8
9
10
class Demo
  member: []
  setMember: (str) ->
    @member.push(str)

a = new Demo
a.setMember('a')
console.log a.member  # ['a']
b = new Demo
console.log b.member  # ['a']

当我们使用new关键词的时候,希望得到的是一个干干净净的对象,可是在初始化b的时候我们发现他的成员变量member已经变成了['a'],这是我们不希望看到的。究其原因就是member是一个数组。解决办法是将这些变量的初始化放在coffeescript的构造方法constructor中。

1
2
3
4
5
6
7
8
9
10
11
class Demo
  constructor: ->
    @member = []
  setMember: (str) ->
    @member.push(str)

a = new Demo
a.setMember('a')
console.log a.member  # ['a']
b = new Demo
console.log b.member  # []

至于为什么这两种写法会产生不一样的效果,可以将coffeescript编译成js来分析。

1
2
3
4
5
6
7
8
9
    (use constructor)                                             |    (not use constructor)
    5   Demo = (function() {                                      |    5   Demo = (function() {
    6     function Demo() {                                       |    6     function Demo() {}
    7       this.member = [];                                     |    7
    8     }                                                       |    8     Demo.prototype.member = [];
    9                                                             |    9
   10     Demo.prototype.setMember = function(str) {              |   10     Demo.prototype.setMember = function(str) {
   11       return this.member.push(str);                         |   11       return this.member.push(str);
   12     };                                                      |   12     };

上面是vimdiff对比出的两种不同写法,第一种是使用构造方法constructor的,可以看到member作为Demo方法的私有变量,在没有用new实例化的时候,这个member是不存在的,所以每一次实例化我们都能得到一个全新未开箱的member。但是第二种写法则不同,在没有实例化Demo类的时候,member对象就已经存在,所有无论你实例化Demo多少次,调用的都是同一个member,也就造成了在多个Demo实例中共用一个member的结果。

后记

假如让我在javascript和coffeescript两种语言之间选择,我仍然倾向于coffeescript,抛开上面的问题不说,它给人编码的时候带来的愉悦是无法衡量的。So just try it!

Mysql 中的 Date Datetime 和 Timestamp

mysql中用于表示时间的三种类型date, datetime, timestamp (如果算上int的话,四种) 比较容易混淆,下面就比较一下这三种类型的异同

相同点

  • 都可以用于表示时间
  • 都呈字符串显示

不同点

  • 顾名思义,date只表示’YYYY-MM-DD’形式的日期,datetime表示’YYYY-MM-DD HH:mm:ss’形式的日期加时间,timestamp与datetime显示形式一样。
  • date和datetime可表示的时间范围为’1000-01-01’到’9999-12-31’,timestamp由于受32位int型的限制,能表示’1970-01-01 00:00:01’到’2038-01-19 03:14:07’的UTC时间。
  • mysql在存储timestamp类型时会将时间转为UTC时间,然后读取的时候再恢复成当前时区。 假如你存储了一个timestamp类型的值之后,修改了mysql的时区,当你再读取这个值时就会得到一个错误的时间。而这种情况在date和datetime中不会发生。
  • timestamp类型提供了自动更新的功能,你只需要将它的默认值设置为CURRENT_TIMESTAMP。
  • 除了date是保留到天,datetime和timestamp都保留到秒,而忽略毫秒。

时间格式

mysql提供了一种比较宽松的时间字符串格式用于增删改查。参考iso时间格式,一般习惯于写成’2013-06-05 16:34:18’。但是你也可以简写成’13-6-5’,但是这样容易造成混淆,比如mysql也会把’13:6:5’也当做年月日处理,而当’13:16:5’这种形式,则被mysql认为是不正确的格式,会给出一个警告,然后存入数据库的值是’0000-00-00 00:00:00’。

手册中还特意提到了一种情况,就是当年的值是0~69时,mysql认为是2000~2069,而70~99时则认为是1970~1999。我感觉是一种画蛇添足了。

总之,以不变应万变,使用’YYYY-MM-DD HH:mm:ss’格式总是不会错的。

原文链接:datetime

Javascript 中使用 Callback 控制流程

javascript中随处可见的callback对于流程控制来说是一场灾难,缺点显而易见:

  • 没有显式的return,容易产生多余流程,以及由此引发的bug。
  • 造成代码无限嵌套,难以阅读。

下面就来说说怎么解决避免上述的问题。

第一个问题是一个习惯问题,在使用callback的时候往往会让人忘了使用return,这种情况在使用coffee-script的时候尤甚(虽然它在编译成javascript时会自行收集最后的数据作为返回值,但是这个返回值并不一定代表你的初衷)。看看下面的例子。

1
2
3
4
5
6
7
8
a = (err, callback)->
  callback() if err?
  console.log 'you will see me'

b = ->
  console.log 'I am a callback'

a('error', b)

在这种所谓”error first”的代码风格中,显然我们不希望出错时方法a中的后续代码仍然被执行,但是又不希望用throw来让整个进程挂掉(要死也得优雅的死嘛~),那么上面的代码就会产生bug。

一种解决方案就是老老实实的写if...else...,但是我更倾向于下面的做法:

1
2
3
4
5
6
7
8
a = (err, callback)->
  return callback() if err?
  console.log 'you will not see me'

b = ->
  console.log 'I am a callback'

a('error', b)

javascript异步方法中的返回值大多没什么用处,所以这里用return充当一个流程控制的角色,比if...else...更少的代码,但是更加清晰。

第二个问题是娘胎里带来的,很难根除。

一种不错的方法是使用一些流程控制模块来将代码显得更加有条理,比如async就是一个不错的模块,提供了一系列的接口,包括迭代,循环,和一些条件语句,甚至还包含了一个队列系统。下面的例子可以表名两种写法的优劣

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
#normal

first = (callback)->
  console.log 'I am the first function'
  callback()

second = (callback)->
  console.log 'I am the second function'
  callback()

third = ()->
  console.log 'I am the third function'

first ->
  second ->
    third()

# use async

async = require('async')

async.waterfall [
  first,
  second,
  third
], (err)->

作为睿智的你,会选择哪一种呢。

[Nginx] if Is Evil

最近用nginx配置中使用if遇到一些问题,碰巧想起以前在wiki中看到的这个页面,虽然我的问题可能和wiki中提到的不同,但是if还是能避免就避免吧

下面的内容翻译自IfIsEvil

IfIsEvil (标题就不翻了,保持原汁原味的比较带感)

简介

if指令在使用在location上下文中时有一些问题。有时候它不能如你所愿,而是做一些完全相反的事情。有时候甚至会引发分段错误。通常来说应该尽量避免使用if

唯一100%可以安全的在location上下文中使用if的场景是:

任何其他情况都可能引发不可预知的行为,包括潜在的分段错误。

需要注意的是if的行为并不是始终如一的。两个相同的请求不会在其中一个上失败而在另一个上成功,通过完善的测试并且对if有深刻理解的话,它可以使用。但是仍然强烈建议使用其他指令来代替。

这些情况下可能你不能轻易的避免使用if,比如说你想测试一个变量,就没有类似的指令可以替代。

1
2
3
4
5
6
if ($request_method = POST ) {
  return 405;
}
if ($args ~ post=140){
  rewrite ^ http://example.com/ permanent;
}

用什么替代

在符合你的需求前提下,可以用try_files代替。在其他情况下用”return …“或”rewrite … last”。在有些情况下可以将if移动到server级别(在这里它是安全的,只有其他重写模块指令允许写在它里面)。

例如,下面的的用法在处理请求时可以安全的修改location

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
location / {
    error_page 418 = @other;
    recursive_error_pages on;

    if ($something) {
        return 418;
    }

    # 一些配置
    ...
}

location @other {
    # 其他配置
    ...
}

有些情况下,使用嵌入式脚本模块(嵌入式perl,或其他第三方模块)来写这些脚本。

例子

下面是一些例子用来解释为什么”if is evil”。不要在家里尝试这些,你被警告过了。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 下面用一些意想不到的bug来说明在location块中if is evil
# 只有第二个header会被输出到响应,这事实上不是bug,它就是这样工作的。

location /only-one-if {
    set $true 1;

    if ($true) {
        add_header X-First 1;
    }

    if ($true) {
        add_header X-Second 2;
    }

    return 204;
}

# 请求会被发送到后端但是uri不会改变为'/',这是if造成的

location /proxy-pass-uri {
    proxy_pass http://127.0.0.1:8080/;

    set $true 1;

    if ($true) {
        # nothing
    }
}

# 因为if的问题,try_files不会起作用

location /if-try-files {
     try_files  /file  @fallback;

     set $true 1;

     if ($true) {
         # nothing
     }
}

# nginx会引发段冲突

location /crash {

    set $true 1;

    if ($true) {
        # fastcgi_pass here
        fastcgi_pass  127.0.0.1:9000;
    }

    if ($true) {
        # no handler here
    }
}

# 捕获的别名在if创造的嵌套location中不会被正确的继承

location ~* ^/if-and-alias/(?<file>.*) {
    alias /tmp/$file;

    set $true 1;

    if ($true) {
        # nothing
    }
}

假如你发现了一个没有在上面列出来的例子 - 请将它报告给MaximDounin

为什么这些问题存在但没有被修复

if指令是重写模块的一部分而且是必须的。从另一方面说,nginx的配置通常来说是说明式的。有些用户希望尝试在if指令中使用非重写的指令,这造成了这种处境。它大部分时间是有效的,但是。。。瞧上面。

看起来唯一正确的方式就是完全避免在if中使用非重写指令。这会破坏很多已存在的配置,所以这没有被实施。

假如你还是想用if

假如你读了上面的内容仍然想用if

  • 请确保你知道它是怎么工作的。一些基础知识可以看这里
  • 做完整的测试

你被警告过了。

原文链接