2012年3月30日星期五

分享一个很久以前的前公司的例子

俺过去曾经啥都干过,包括开发、排查Bug、数据库部署维护、服务器采购、机房迁移,乃至公司的电话机布线和程控机的编程……好吧,事情是杂了些,但不代表不专业。

不过销售为主导的公司,一定会发生一件事情,就是在技术上销售比技术还要牛叉。在以前的公司就曾经有这么一件事情发生在我身上:

某天开始,服务器每天都会时不时的速度很慢,然后就突然无法访问。该问题长时间排查都找不到原因,现象倒是挺明确的:不存在数据库连接泄露,某种偶然情况下Web进程内存占用会突然缓慢持续增加,当到达接近1GB时会突然崩溃,日志显示内存不足。

这个现象很不好理解,因为:

  1. 并非必然发生,而是阵发性的;
  2. 网络带宽只占用了30%-50%左右;
  3. 内存并没有占用满32位操作系统的用户内存空间上限,甚至连一半都没有用到,怎么就内存不足呢?
  4. 代码拉网筛查,以及跑profile也没有发现任何内存泄漏;
  5. 按道理来说,.NET托管内存是不可能存在泄漏的,而Web应用很少会有大量对象长期生存,也不可能占用内存至内存不足。

无论原因如何,也不可能是网络带宽的原因。可是搞笑的是,销售的一个头头居然大发雷霆,发邮件短信说技术反应迟钝不作为,这么久都不增加带宽。当时真是非常无语,尽管之前已经通报过情况,也说明不可能是带宽的问题。但既然已经惊动老总,只好按照其要求将带宽翻倍。可想而知,该现象依然如故,于是就没话可说了。

此问题非常棘手的主要原因是现象1,导致你只能一直远程并盯着内存量,当发现开始增长并持续一段时间之后,就要果断做Dump。可是,有可能你正好手头有事或者谁跟你说一件事,这个现象就错过了;又或者做Dump太晚了,导致CPU已经开始大量占用无法启动Dump程序;又或者Dump了半天快结束的时候进程被IIS自动Kill掉了;又或者Dump的太早什么都没有Dump出来。

经过很长时间的联系,终于熟练的抓到了一个Dump,结果发现实际内存占用量非常少,大约只有300M左右,但是内存提交量却达到了2G。最后实在没辙了翻查GCHeap,结果找到少量极其巨大的string对象,比如20MB。这些string对象指向了一个位置:日志。原来在系统中有一个数据库表的字段,是用来存放最后一次某些特地操作的。按照设计,这个字段应该只保留最后一次,但实际情况是该字段不知道为何被改成一直累加。于是乎,一些频繁访问的用户就会累积出这种巨大日志记录。只有当这些用户频繁访问的时候,才会造成系统崩溃。这很好的解释了前面所说的5个现象,尤其是阵发性。

也许有人会问了,那GC不应该回收么?对,GC会回收,但是GC回收会有一个副作用,就是CPU占用会变高,导致系统变得很慢。此外,还有一个无法在32位操作系统中很不好解决的问题:由于.NET设计上将大于85kb的对象放在一个特殊的Large Object Heap(LOH),而这个Heap和普通的GCHeap有一个明显的不同,就是GC发生时并不会进行对象拷贝(也就是碎片整理)。最后,连续的内存地址将会全部碎片化,此时如果需要分配大对象的时候,将会再也找不到任何连续地址用于存放此对象。虽然所有空余空间加起来远远超过几百兆,你也可能无法分配一个30兆的内存空间出来。于是才有了明明内存占用很少,却报告内存不足。

你看,这个问题挺技术的吧。可偏偏有那种什么也不懂的人,跑出来很技术的要求增加网络带宽。从这个事情上,我明白到合作的事情,你必须相信你的伙伴在他的领域内是专业的,不要傻了吧唧的在那里指手画脚。如果你不信任,那你就应该立即换人,并同时承担看错人的成本(包括新人和旧人)。否则,指手画脚除了显示出你的不专业,搞坏团队气氛之外,不能带来什么好处。除非,你认为以飞机失事的概率成功解决一件事情能算是成功。

哦,对了,这种事情从来不会停止发生。比如说上面说的那个销售老大技术真是完全不懂,否则根据我在通告中说明的“内存不足”异常,可以在带宽倍增无效之后,继续要求给服务器增加1TB的内存,然后再增加100个CPU和600个网卡。最后实在不行了,还可以赖.NET不如Java先进,要求更换至Java平台然后使用Oracle数据库。至于操作系统嘛,可以换成EXASOL EXACluster OS……

Linux撞鬼集4之IFS

前阵子自建CDN,因为有攻击导致某地点某IP不可用。在屏蔽攻击后重新需要给用户重新分配IP地址,但是分配之后一直都没有成功。通过查看配置文件,我们确实配置成新的IP地址了,并且确实已经“成功”的重新启动bind服务。但是用户持续报告不可访问,我们直接找ip地址发现实际上仍然是旧的。

一开始我们怀疑可能是不是TTL错误,或者什么别的原因导致用户访问的DNS服务器没有刷新新的解析记录。经过一阵紧张的nslookup和dig之后,还是没有能成功找出问题所在。最后修改named.conf当中的日志级别为debug,重新启动之后发现实际上有一个区域的解析记录并没有成功加载。日志大概意思如下:

unknown type "xxx.xxx.xxx" for record NS (大概这意思吧)

打开配置仔细一看,原来该区域下面应该有一个NS解析记录,原本应该是:

            NS       xxx.xxx.xxx

结果变成了:

NS xxx.xxx.xxx

这一条解析的意思是,当前要解析的域下面的域名服务器是xxx.xxx.xxx。但是由于前面的空格去掉了,结果按照空格划分,第一个表示二级域名的字段从空变成了NS,而记录类型则从NS变成了"xxx.xxx.xxx",而第三个表示解析结果的字段则从"xxx.xxx.xxx"变成了空。于是这一个域名解析记录加载失败,这也导致了后面更改了的解析记录实际上从未被正确加载。之所以域名服务器仍然返回旧的IP,是因为过期时间比较长,在得不到我们的正确解析时,其它的DNS服务器将沿用原来已经有的域名解析结果。

那么,这个空格为什么消失了呢?如果是某个人手动修改的,其手动意图又是啥?经过调查之后发现,原来有人写了一个修改解析地址的脚本,正是这个脚本造成的问题。而之前就已经发现有此问题,解决的办法是通过运行脚本之后手动添加空格来解决,而这一次却忘了。

具体造成问题的脚本命令是read,这个命令十分神奇,会自动根据IFS来分割字段,然后将字段重新组合成一个字符串。用C#来描述,就是:
return string.Join(IFS[0], Console.ReadLine().Split(IFS, StringSplitOptions.RemoveEmptyEntries));

解决的办法很简单,就是将脚本改写成:

while IFS=""; read LINE
do
#  something
done < afile

关于IFS在while上面赋值,我想了一下,原因应该是这样的:
如果在独立一行修改,则导致从此之后的运行环境发生改变,会影响并改变后续所有命令的默认行为。而如果在while上面赋值,则因为是folk一个子进程,并且在子进程中运行修改的缘故,并不会导致外部(当前脚本)环境的变化,也不会改变后续命令的默认行为。

这种环境变量影响程序行为的问题,在Windows算比较难以注意到的。一般也就是PATH之类的变量会导致能否找到可执行程序,但很少会影响程序本身正常行为的。对于read来说,默认按照IFS分割并重组输入,确实让初学者难以注意到。

一句话

人生就是一砣砣屎等着让你吃,你要么找别人替你,要么就在吃下一口之前赶紧大喘气。

都掉钱眼里了,就别跟我谈什么理想梦想的。

不要将自己的无能怪罪到别人头上,尤其别人是在帮你的时候。

在说别人没有XX的时候,最好先看看自己有没有。(XX=卵、智商、情商、能力……)

话不要随便乱说,因为朋友的敌人的敌人,可能同时是你的敌人和你朋友的朋友。

沟通是倾听,是从别人的视角看问题。

凡是使用外部归因法的,都是懦夫、侏儒、智障。

有问题的永远是我自己,别人永远都是没有问题的,这样才能做好事情,会做人。

2012年3月29日星期四

技术宅分析养老金

据说是郎咸平的文,至于是不是,我没有考究,我关心的是里面数字的计算:
继续工作20年,退休活命20年,当前平均工资4000,退休后保持生活质量需要243万,实际养老金只有37万。

首先说我的结论,那就是赶紧移民吧。且不管里面别的地方是否正确,但是有那么几点是一定正确的:

  1. 公务员退休保障是普通人的好几倍,已经形成公务员阶级和屁民阶级。未来某个点上面肯定要达到势不两立的地步;
  2. 最低收入者需要交的保险比例比寻常人高也是不争的事实(北京貌似按照1600的最低基数交,就算你只拿1200的工资);
  3. 各类社保亏空是不争的事实,未来一定会通过增发货币进行账面名义值弥补,但一定会引起持续的高通胀,乃至超高通胀(乃至引发爆点);
  4. 各种不明不白是一定的:比如你移民,公司交的那部分就没了;跨地区也是被截留一部分;
  5. 公积金账户年利率似乎只有0.36%这种级别,至少不是一年定期的利率。可是大家都听说过社保基金入市吧?那么用我们的钱赚出来的利润怎么就被侵吞了呢?如果亏了呢,算谁的过错,最后又是谁来买单?(答案:没人负责,我们通过货币增发补充账面亏空-〉通货膨胀-〉货币实际价值下降-〉生活质量下降的路径来买单。)

实际上我一直认为,如果这些钱如果发到每一个人的手上,全部存1年定期,也比放在政府账户要有更高的保障。如果政府真心为我们好,那就不要交什么社保费,直接动用税收给我们发钱就好了。当然,如果是每个人一个独立账户,清清楚楚,那也可以考虑。

之所以现在大面上大家都没有反应,那是因为传统文化中的“国〉家〉个人”的部分作怪,个人权利意识从未真正觉醒。尤其对于底层的群众来说,经常不自觉认同可以为了大家的利益牺牲个人利益这种扭曲的观念,只有当被强拆的时候才会发现周围的人是那么的冷漠。如果放在西方,这种社保制度早就上街抗议了,因为个人利益受到了极大的损害,而且不明不白。

扯远了,咱还是扯数字。上面那几个数字在其假设上面是正确的,计算方式如下:
令G = 4000, k = 1.03, 则有
G20 = G*(k^20) = 7224.45 (20年后的月工资)

因为每年增加3%这个数字比较小,可以认为近似线性增加,于是20年工资总和为(略小于):
T20 = (G+G20) / 2 * 20(年) * 12 (月) = 1,346,933.39

令p = 8%+20% = 28%,则有实际保险名义值 J20 = T20 * 28% = 377,131.35,约38万。

20年后至40年后这段时间,如果每个月都需要现今的4000元,则此期间名义货币值总需求量是T20_40 = (G40+G20) / 2 * 20 * 12,其中
G40 = G * (k^40) = 13048.15,因此
T20_40 = 2,432,711.53,约243万。

从计算结果看基本吻合,但是实际上这里面有几个比较大的漏洞:

  1. 大部分人将工作38年,而不是20年;
  2. 现在开始工作的人,其人均寿命是68岁;(注意,2012年的人均寿命是指预期2012年出生的这群人的平均寿命,而不是这一年死亡人口的平均寿命。)


也就是说,今年刚工作的本科毕业小鬼,将会干38年的活,然后活8年。于是:
G38 = G*(k^38) = 12,299.13
G46 = G*(k^46) = 15,580.17
T38 = (G+G38) / 2 * 38 * 12 = 4,451,375.98
J38 = T38 * 28% = 1,246,385.27,约124万。
T38_46 = (G38+G46) / 2 * 8 * 12 = 1,338,206.82,约134万。
也就是说,账面缺口大约10万。于是差距已经小了很多,此外考虑实际账户资金是存在一定的利率的,比如存1年定期,这10万的差距基本上是可以找回来的。

呃,如果这样的话,为什么还要考虑移民呢?因为:
如果你不幸寿命没有达到60岁,那么……
目前有一大批的退休人口,实际上在过去是1分钱的保费都没有交。或者更准确的讲,是那些计划经济时代的老一辈,政府就没有专门的账户和预算来应对养老的事情,基本上算是比糊涂账。这部分的内容才是目前账面亏空的根本原因;
还是那句话,不清不楚,随便侵吞个人权益。于是你贡献的时间越长,损失可能就越惨重。比如说,未来如果出一个规定,说必须回原籍才能拿养老保险……

当然,如果你维权,就会遇到一个强大的政府,完全不按照法律行事。比如我就听过这样的对话:
屁民:按规定应该可以办的。
领导:(一拍桌子)规定!我打开这个抽屉里面的文件有一个规定,打开另一个抽屉里面的文件又有另外一个规定,我想要什么样的规定还不简单!

我国立法形式基本上就是形式,完全无视宪法,宪法也只能代表装精神一下。比如没有宪法法庭,也没有违宪救济途径。所以你既可以说有法有天,也可以说无法无天。


几个简单常用的Linux命令——日志分析篇


同事说能否分享一下linux日志分析的心得,尽管我也只是初学小菜,不过既然问到了,那就献丑了。

linux里面有那么几个常用的命令,在大部分日志分析场景里面都会用到:

cut  按照特定分割符分成若干列,取出其中的某一列。
     -d" " 表示用空格作为分割符默认好像是逗号;
     -f8   表示取出第8列。

sort 按照特定的分隔符分成若干列,按照某一列进行排序。
     -d" " 表示用空格(默认应该就是空格),-f1,2表示用第1、2列输出;
     -g    表示按照数字进行排序;
     -r    表示逆序;(默认从小到大);
     -k8   表示根据第8个字段进行排序;(默认只根据第1列排序,可以通过设置IFS来指定分隔符)
     -k1,3 表示根据第1至3个字段之间的内容进行排序;
     -u    表示相同的元素只输出一次。

uniq 如果下一行重复则消除。
     -c    表示统计出现次数(重复一次计数为2)。

head 输出最开始的几行。
     -n8   表示输出头8行。(无参数时默认10行)

tail 输出最末尾的几行。
     -n8   表示输出尾8行。(无参数时默认10行)

wc   输出指定文字出现的次数。
     -l    表示输出出现该文字的行数而不是次数。

grep 输出包含指定文字的若干行。
     -i    表示大小写不敏感;
     -e    表示使用正则表达式;(等同于egrep命令)
     -v    表示输出不等于制定文字的若干行;
     -m8   表示指定文件中的输出前8个;
     -a    表示如果是二进制文件,则仅对比里面是文字的部分;(比如要找squid缓存文件当中记录的,与该文件对应的uri时就特别有用);
     -r    表示递归搜索所有文件夹中的指定文件;
     -H    在每一行输出中均包括文件名;(搜索单个文件时默认不输出)
     -h    在每一行输出中均不包含文件名;(搜索多个文件时默认输出)
     -l    仅输出有命中的文件名;
     -L    仅输出没有命中的文件名;
     -B8   输出命中该行之前的8行;
     -A8   输出命中该行之后的8行;
     -c    仅输出命中次数。

上面这个命令组合通常就可以进行大部分的日志分析工作,下面举例说明。

我们假设日志名称为squid.log,格式如下所示(有点像Squid,但实际上不是真实的日志格式,只是为了便于说明):
- - [2012/1/1 04:23:44] 223.33.54.123 "GET /pages/head.htm HTTP/1.1" {www.fix.com|http://www.google.com/search} 200 - - TCP_HIT:NONE "Mozilla/4.0"

例1 查看最后若干个访问"/pages/head.htm"的IP都来自哪里:
grep -i "/pages/head.htm" squid.log | tail -n20 | cut -d" " -f5
注意:

  1. 日期和时间当中有一个空格,因此是两个字段,而不会因为方括号而认为是一个字段。同样的,单引号双引号也不会影响以空格为分割符的分割;
  2. 如果文件非常大导致明显非常慢,可以尝试用tail作为开头进一步限制,如:

tail -n1000 squid.log | grep -i "/pages/head.htm" | tail -n20 | cut -d" " -f5



例2 统计最后若干个访问"/pages/head.htm"的IP的次数:
grep -i "/pages/head.htm" squid.log | tail -n1000 | cut -d" " -f5 | sort | uniq -c | sort -g
备注:这里的技巧是,先要sort,然后再通过uniq -c来统计次数,然后再通过sort来进行排序。这是因为对于uniq命令来说,之比较上一行和下一行,而不会考虑交错的情况,例如对于文件tmp如下:
A
A
B
B
A
A
C

用uniq -c tmp来检查,输出如下:
2 A
2 B
2 A
1 C

而实际上我们希望的输出类似如下:
1 B
2 C
4 A

于是只能够通过sort先将相同的内容放到一起,然后再用uniq来统计。sort命令当中的-u参数并不会统计处数量,于是必须使用uniq命令。同时uniq输出的顺序是原始文本出现的顺序,而不是根据数量进行排序的,因此最后还需要用sort进行排序。


例3 统计最后若干个访问"/pages/head.htm"的IP中,出现次数最多的前三名以及其次数:
grep -i "/pages/head.htm" squid.log | tail -n1000 | cut -d" " -f5 | sort | uniq -c | sort -gr | head -n3



例4 统计后端返回状态为503的访问次数:
cut -d" " -f10 "/pages/head.htm" squid.log | grep 503 | wc -l


例5 统计后端返回状态为200,且访问"/pages/head.htm"的HIT和MISS的情况(日志中TCP_HIT:NONE的那一列)
这一个需求比较复杂,我们需要动用上面没有介绍到的另一个命令:awk

awk  对每一行按照指定分隔符进行分割,并根据指定的条件输出指定的内容。
     -F" " 表示输出以空格作为分割符。

完整命令格式如下:

awk [条件] ['指令'] [待分析文件]
之所以指令是可选的,是因为参数中还有一个可以指定指令文件的参数;而待分析文件可选,是因为可以通过管道提供待分析文件。

其中的指令部分比较复杂,完整的这里就不介绍了,请自行谷歌之。这里介绍非常简单的几个常用部分。
$1       表示第一个字段
$1>2     表示第一个字段是数字且大于数字2
$1>"2"   表示第一个字段大于字符2,也就是说"12"<"2"
$1==$2   表示第一个字段等于第二个字段
&&       表示与
||       表示或
a { b }  表示如果前面的a部分匹配,则执行b部分
print    表示输出整行
print $1 表示输出第一个字段
;        表示两个指令之间的分割

于是
$1==200 { print; }
就是说第一列等于200就输出整行。

对于本示例,将需要使用如下命令:
awk -F" " '$10==200 { print; }' squid.log | grep -i "/pages/head.htm" | cut -d" " -f13 | sort | uniq -c | sort -g


例5 统计UserAgent的情况,但前面的访问路径处可能包含不定数量的空格,比如某些访问没有发出HTTP/1.1,于是只有"GET /pages/head.htm"。


这个问题很容易想到:啊,如果能够忽略双引号中的空格多好啊!嗯,这种思路应该也是可以的,但是比起下面这种思路来说,有的时候真的太复杂了。
cut -d\" -f4 squid.log | sort | uniq -c | sort -g

“……靠,还能用双引号做分割符,咋就没想到呢?”好吧,再提示一下:每一个命令可以考虑使用不同的分割符,而不需要每一次都用同一个分割符。

2012年3月28日星期三

Linux撞鬼集3之$()内嵌加管道(尚未解决)

由于自己比较菜,写linux脚本的时候总遇到一些很撞鬼的事情,比如说:
$(echo "echo hello")

这个的执行结果是:
hello

但是执行下面这个命令:
$(echo "echo hello | more")

结果你以为仍然一样,结果却是:
hello | more

嗯?为啥是这样的呢?抱歉,我不知道为什么。从我目前的认知来说,我以为$(xxx)的意思是执行xxx命令,并且再次执行该命令的输出。用第一个例子来描述,那就是:
echo "echo hello"

将会输出:
echo hello

而执行 echo hello 的结果,将会是:
hello

也就正好可以解释第一个例子的执行结果。可是到了第二个例子,我们推测
echo "echo hello | more"

的执行结果是:
echo hello | more

而上述语句的执行结果应该是
hello

这是因为该输出只有一行,more不会起任何影响。但是实际上第二个例子的结果却等同于:
echo "hello | more"

嗯,这就奇怪了,bash怎么会如此识别呢?如果说是自动根据IFS来添加双引号/单引号,那也应该是等价于下面这样的命令才对。
echo "hello" | "more"

好吧,我假定$()操作符将会根据输出结果按照IFS分割成两列,第一列作为命令,剩下的都作为参数,这样倒是能解释结果。可是,我要如何写才能得到等价于hello | more的执行结果呢,或者说达到这一个目的呢?

……呃,这个,非常抱歉,google这个玩意儿对于学习linux脚本编程天然鄙视。比如搜索“$()”,结果是木有啊木有,更不要说搜索“$() |”这样的玩意儿了……(泪奔ing)

P.S.:
$(echo "echo hello" | more)这个是可以的,但是这两者其实并不等价。比如说有一个有3行文字的文件a,执行cat a | head -n 1,现实的是一行。但是执行$(echo "cat a" | head -n1),却把所有文字都显示出来了。为啥?不知道,GOOGLE不告诉我……

2012年3月18日星期日

出逃

我们说投资要跟着巴菲特,就是因为这些人通常见多识广,见解可能更加正确。照这么说,只要有可能,学习他们的生活方式也是应该的。因为通常来说,他们的生活方式可以让他们比我们活的更有质量、更加长寿,后代更有可能培养得更好。

今天看到一个新闻,说,可投资净资产1000万以上的中国人“14%的人已经移民海外,另有46%的富人正在计划或者办理移民的过程中。等到后者也完成移民手续,那么,每5个有钱的中国人中,将有3个是外国国籍。”而“在2万名拥有可投资资产超过1亿元的中国富人中,27%的人已经移民,另有47%的人正在考虑移民。超级富人高达74%的移民比例,真是一个惊人的世界纪录,恐怕连俄罗斯或者动荡不安的中东国家也望尘莫及。”

呃,这个估计跟法73条这类的问题有莫大的关系。尽管越往高处的人,这类感受越深刻。但也不代表像我们这样的人没有感受,比如我就去拿AU的PR了(老早的事情了)。但当时考虑到一些私人问题,并没有立即成行。这么几年下来,几乎都已经快不怎么注意与此有关的事情了。除了偶尔获知现在申请已经比当年难上许多,审批的速度也不比当年神速的时候,会暗自庆幸一把。

但是最近抽筋了解了一下情况,发现原来说小孩的PR申请最多2个月就下来了,现在变成了14个月。原来2周能下来的牛奶金,现在有人居然4周都没有下来。由此可见近期到达AU的人数真的变多不少。至于这是否华人的功劳,那就不清楚了。但可以大胆猜想,申请小孩PR的人这么多,说明去那边的人真的多了很多,估计AU的房价大概就跌不下来了至少。甚至如果说涨价了,那也没有什么好奇怪的。

原本AU就已经是发达国家中生活成本非常高的国家之一,现在这么搞法想想头皮有点发麻。但一想到国内吃喝玩乐坐火车呼吸空气皆不安全(尤其是对小孩来说),再加上如果在网上不小心表达了这种不满,还不知道会不会被消失家人却不知道,就觉得还是这一坨狗屎更恶心人。于是走人的想法还是那么的坚决,尽管很不情愿。

是啊,如果能够有免于恐惧的自由,不用害怕吃错东西、喝错水、看错书、上错学、吸错一口空气以及说错哪句话,为什么要拖家带口、离别父母、长途跋涉、艰难开拓呢?谁不愿意好好的呆在温暖的窝里舒适的生长呢?可现在,我居然是看着许多没有赶上趟的、或者不具备移民资格的同学,感叹道:可惜啊,没能离开祖国!

---