2012年10月31日星期三

揭露爱大爱D3能量近视疗法的欺诈本质

题注
  1. 本文首发于旺旺网的康建园板块,旋即被网站管理人以“涉嫌推销”而连文章带帐号一起封杀。后来经申诉后释放了回来。为了避免将来再由于“涉商”原因被删,还是整理到自己的博客中比较放心。关于旺旺网的一些令人难以理解的做法,我将来会专门撰文分析。 
  2. 为方便国内网友访问,自本篇博客起,我的博客文章将会同时发布在新浪博客(注:凡是转贴的文章将仅会发布在新浪博客,只有原创或对佳作的全文翻译才会同时发布到这里。 

我小孩今年8岁,上三年级,眼睛已经275度了。我们都很着急。一个月前,孩子妈妈接了一个推销D3近视疗法的电话,我们三人就一同去了八百伴附近的那个公司去体验了一番。结果不出我的预料,完全是忽悠。为了让众多近视孩子的父母作出正确的抉择,本文对其各种忽悠手法作一彻底的分析。 在开始之前,首先申明:我不是专业的医生。所有分析都是基于逻辑推理和我对科学的认识,尤其是概率统计、心理学和证据学方面的认识。

D3能量近视疗法--骗术的集大成者

D3疗法使用了诸多骗术,我听着她们的忽悠,心里感到又好笑又叹为观止,简直是一个骗术大荟萃嘛。

1. 名人效应

到达现场后,除了给孩子测视力以外,工作人员就开始拿了一本手册开始介绍。首先介绍的是汪道涵的孙子,有和汪老的合影,还有一些其他名人,我不记得了,反正一大堆名人或者名人之后。这开场白挺老套,也挺震撼的。

2. 心理暗示

大家一定看过一个高路洁的广告,将一个贝壳上涂上牙膏,放到酸性液体中浸泡,然后拿出来敲,涂高路洁的敲不破。还有一位明星做细菌检测的,一根棍子在牙齿外面一照,就象拍X光一样。看来牙膏商最会使用这种手段,但是D3也毫不逊色。她在介绍说这个D3设备能够“活化视觉细胞”的时候,“未活化”的细胞用的图示是一张显微镜下的细胞图(当然不知道是什么细胞了);但是在介绍”活化后“的细胞时,用的是一副电脑画出来的一堆红色球体,看上去鲜艳欲滴,尺寸也比正 常的细胞大了2~3倍。

当然,我现在如果这么说,你肯定会说,这仅仅是”示意“,可以理解。但是在现场结合其他的骗术,这种心理暗示的效果不可小觑。

3. 假专利

硬伤来了。介绍材料中说到,D3的技术是获得中、美、英等若干个国家专利的技术。我当即就问,能不能给我中国和美国两个国家的专利号,我去了解一下。那个介绍的工作人员当即就脸色发白,顾左右而言它了。绝大多数家长到这种地方,在心理上是极其乐意相信它是真的,因为只有它是真的,才能给你的小孩以希望。听到这个东西获得那么多国家的专利,心里一定乐滋滋的,估计没有什么人提出这个问题的。

我回家后使劲搜索了一番,找到两个关于D3的专利,其中有一个是在阿里巴巴上找到的(顺便提一句,D3能量棒,也就是他们声称具有奇效的工具,在阿里巴巴上批发价两万多,一套起批)。该专利是一个实用新型专利,也就是说“对产品的形状、构造或者其结合所提出的适于实用的新的技术方案”。专利中,最有价值的是发明专利,实用新型没有太大的意思。D3的这个专利的摘要就是这个装置内部安装了一个强磁铁,这个装置的设计使得这个磁铁能够很好的发挥作用,而且更换很方便云云。最重要的是,这个专利仅仅是提交状态,也就是说,所谓的专利号实际是一个申请号,而非授权号!

另一个专利更加好笑,是在所谓的D3官方网站www.yes-d3.com上找到的。上面列出了所谓的中美英日法德等十国的专利证书,都是图形方式的,根本看不清内容。但中国的两份专利的标题赫然是“外观设计专利“!这种东西也拿得出手!

还有一条有意思的脚注,该网站申明“已與珠海愛大愛解約,再無任何合作關係,亦不再提供相關諮詢及保障”。现在不知道这个“程氏量子科技研发中心”和“爱大爱”哪个是始作俑者,哪个是李鬼了。:-)

建议大家对产品中凡是声称有专利的,都查一查(比如“阿瞳二代”什么的),非常简单的可以鉴别李鬼。国家知识产权局的专利查询地址是:http://www.sipo.gov.cn/zljs/

4. 假专业知识

上面提到的这个“程氏量子”非常的有意思,它的一段宣传我一定要记录在案,真正是令人叹为观止: 

本公司运用爱因斯坦(Einstein)的光量子公式E=MC2所得之光的波动性本身就不是连续的,而且有粒子性,无论是光子或光波,爱因斯坦均称之为光量子(light quantum)是辐射能量的最小单位,其“不可能被毁灭”理论得到公证,也就是一种永不消失的能量(D3)即光能,后来与戴布劳格力(De Brogllie)的【物质波】理论相结合,因同时具备波动性的能量传递与信息分子吸收之特性,产生互动,因而诞生了【量子医学】...... 

且不说该描述狗屁不通,根本不是在描述物理学原理,仅仅是堆砌了一堆似是而非的专业名词,就算描述是准确的,我的印象中爱因斯坦的质能方程也是和原子弹爆炸的原理相关,怎么扯到治疗近视上了?

回到我去“体验”过的那个治疗中心。她们明显没有yes-d3那么有“创意”,但其忽悠外行的功力也丝毫的不逊色。比如,提出了“先天散光”和“后天散光”的概念。据她们说,先天散光是无法治愈的,后天散光是由于写字姿势不正确(歪头)导致的,她们是可以治愈的。然后,她拿出一副黑漆漆的眼镜让我小孩带上,视力居然神奇地从0.2变到了0.6!她就据此下结论,这个是散光,如果纯粹的近视,效果不会那么明显。当时孩子妈妈很激动,说这个眼镜能不能卖给我们一副,她说,只要你签约就送的。后来我回家一查,这个东西就是小孔眼镜,也就是小孔成像的原理,与近视眼眯眼睛才看得清楚一点是类似的。治疗近视没有效果,戴久了还可能引起眼睛疲劳加深近视!她居然还建议上课可以常戴(只要不是运动的时候而且光线良好就可以)!

她们还建议,去看早上六七点钟的太阳,说那个时候太阳光具有丰富的“远红外”光,是“生命之光”有利眼睛保健。不知是什么深奥的理论。我询问D3是什么意思,她含糊其词的说就是指的是红外线。我看出此人并非具备必须的理工科背景,就没有深入询问。但她们按摩用的棍子不是磁铁吗?怎么扯上红外线了?不解。

另外,她们在描述其疗法功效和一些视力保健的方法的时候还说了一些毫无根据、仅仅为了体现其专业性的话,为了减少误导,我举两个例子:
  • D3疗法活化80万个视觉细胞和多少千万个视神经之类的说法纯属信口开河。我不知道她们哪里来的数据,而且所谓的“活化”明显是化妆品广告用词,怎么会出现在正规的医学疗法的描述中? 
  • 她们强调绝不可戴眼镜,说带上眼镜后眼睛的调节功能就会适配眼镜,使得治疗前功尽弃。这个我是坚决不相信的。我也听说过很多“眼镜尽量不要戴,戴了以后近视越来越深”这种说法。但我认为导致眼睛度数加深的原因就是导致开始近视的原因(比如遗传、用眼习惯、营养等等),不应该是戴眼镜。希望大家破除这种迷信,向专业的眼科医师咨询是否要戴眼镜!

5. 利用统计知识盲点

统计知识的盲点是非常可怕的。比如爱大爱的宣传中,说“由于高度近视而致盲失明的青少年达30万人之多”,这种说法有没有依据?是多少大的基数中统计出来的?所有的近视人群中有多少是所谓“恶性”的近视(也就是会导致黄斑变性、视网膜脱落等严重问题的)?各种宣传往往夸大其词,或者直击家长心理最脆弱的要害,使你丧失分析问题的理性。

举个例子:某种疾病,在城市人群中的发病率约为十万分之三,且近年来有上升的趋势。最近国际上出现了一种针对此种疾病的检验方法,约95%的患者能在早期检验出来,从而得到及时的治疗。但是,该方法有0.1%的误判率,也就是说,每一千个未患此疾病的正常人也会对此检验作出阳性反映。现在,某甲去医院做了这个检验,结果呈阳性。请问该人患此疾病的概率是多少?

这个题目非常有意思,可以看出你对概率统计的理解是否正确、全面。现代医学对统计的依赖极其严重,大家应该正视这个问题。

6. 纯骗术

她们在做体验治疗之前,给我小孩做了一次视力检测,理疗以后又做了一次。但是两次使用的方法不同。第一次是有不对的就直接退一行,总之把视力测得越低越 好。第二次看的距离比第一次近,而且是循循善诱,看不清楚慢慢看,实在不行了,就写上0.3(+2),意为,0.3这行看对了两个。孩子妈本来对她们印象 挺好的(因为她不是搞理工科的,对一些技术性忽悠没有在现场识别出来),但在这个问题上引起了她极大的反感,当即指出她们是在忽悠。她们就说,这是因为鼓励小孩,每次有小的进步就要记录下来云云,还给我们看记录,证明她们的诚实。记录上有类似0.8(+2)的记录。

我们回家议论这个问题的时候想到,0.3的时候,一行也就3个字母,对两个怎么地也过半了,0.8那行看对2个,算是0.8了??为什么不老老实实地写 0.7?而且每次记录都是要小孩签字。我们在体验的时候看到两三个小孩自己过来接受治疗,没有大人陪,这个记录的可信度就存疑了。我还问她们,是不是更换视力表,以免小孩都背出来了,她们声称是更换的,但我没有看到她们的视力表是可更换的,而且此时我已经完全不信任她们了。

7. 它为什么有效?

我说了那么多,有人会问,那么多人去治疗过了确实有效,你怎么解释?我的结论如下:
  1. 我认为网上绝大多数(但不是全部)是托,或者“被托”,也就是因为吃了她们的安慰剂,认为有效的家长被动地被她们引用了。给大家看篇D3相关的文章,很有意思:http://www.33ruanwen.com/html/?3984.html
  2. 孩子的体质和其他条件千差万别,对某些孩子有效的,对你的孩子不一定有效。而且整个“治疗”过程非常复杂,包括有运眼体操、营养配合(很多家长给小孩吃蓝莓等 等)、体育锻炼等等。你怎么知道是这个D3的穴位按摩疗法有效了?即使是有效果了,我也觉得是因为花了那么多钱(注她们的报价是一个疗程将近5000元,两个疗程将近9000元,基本上每次“治疗”200多元),起到了一个督促作用,让你去全方位地运用各种保健、营养、运动、改善用眼习惯等等。问题是,这些事情只要是重视的家长都在帮小孩做,需要这个骗子来协助吗?

2012年3月7日星期三

复制DVD到mp4文件

最近有需要复制DVD到U盘给电视机播放,网上搜索了一下,找到这个:

http://luy.li/2011/05/15/dump_dvd/

觉得不错,就编辑记录在下面,备用。

--------------------- 原文 ---------------------

注意:此脚本只是一个对我来说刚刚够用的脚本,并不是一个通用的方案,如果你的源DVD里有多语言、多字幕之类的,很可能需要修改参数才能正常运行,另外输出文件的码率、画面长宽、声音采样率等,也需要按实际情况修改。

给儿子买了12张巧虎的DVD,但是这年头,已经很少见DVD播放器了,电视机都是直接插U盘的,所以打算把DVD里的内容dump到U盘里(mp4格式),再进行播放。我的盘是按故事分段的,所以我也按段分成不同的mp4文件,一个盘的内容放在一个目录里。
用到的几个命令:
  • lsdvd命令可以取得DVD的标题、语言、字幕、分段等信息。
  • mencoder是个强大得一塌糊涂的视频/音频编码工具。
  • eject命令可以弹出光盘,放在脚本最后最合适了。

上脚本:

#!/bin/sh
 
DIR="/media/sda1/qiaohu"
DVD=`lsdvd`
title="`echo "$DVD" | grep "^Disc Title:" | cut -d ":" -f2-`"
title=${title:1}
chapters="`echo "$DVD" | grep "^Title" | awk -F"[ ,]+" '{print $6}'`"
echo "$title | $chapters"
T="$DIR/$title"
if [ -d "$T" ] ; then
 i=1
 while [ -d "$T$i" ] ; do
  ((i++))
 done
 T="$T$i"
fi
mkdir "$T"
i=1
while [ $i -le $chapters ] ; do
 echo $T/$i.mp4
 mencoder -of lavf -lavfopts format=mp4 -oac lavc -ovc lavc -lavcopts aglobal=1:vglobal=1:\ 
vcodec=mpeg4:vbitrate=800:acodec=libfaac:abitrate=96 -af lavcresample=48000 -vf dsize=\
720:540:0,scale=0:0,expand=720:540,harddup -ofps 29.970 -srate 48000 -o "$T/$i.mp4" dvd://1 \
-chapter $i-$i
 ((i++))
done
 
eject

2012年2月24日星期五

如何在Linux下编码WebM格式的视频

WebM是Google推出的为HTML5设计的视频格式。它使用了Matroska容器格式、VP8视频编码和Ogg Vorbis音频编码。 它是开源且免费的,目前获得了除Microsoft和Apple以外的主流厂商生产的浏览器(Firefox、Chrome和Opera)的支持。

为了在Linux下将视频文件转码成WebM格式,你需要ffmpeg 0.6以上版本。一般这个不成问题,例如在Ubuntu 10.10上自带的ffmpeg已经内含对WebM的支持。除此之外,你还需要libvpx支持,这也在系统的软件库中提供了。

首先,建立一个ffmpeg配置文件(假设你要编码的是720p格式,若不是,需修改此配置文件):
vcodec=libvpx
g=120
rc_lookahead=16
level=216
profile=0
qmax=42
qmin=10
vb=2M
#ignored unless using -pass 2
maxrate=24M
minrate=100k
将以上内容保存到/usr/share/ffmpeg/libvpx-720p.ffpreset文件中。如果此文件已经存在,则可察看一下现有版本与以上内容有何差异,并根据ffmpeg的手册进行必要的调整。

现在就可以开始编码了。依次执行以下两条命令:
ffmpeg -i input.mp4 -vpre libvpx-720p -b 3900k -pass 1 -an -f webm -y output.webm

ffmpeg -i input.mp4 -vpre libvpx-720p -b 3900k -pass 2 -acodec libvorbis -ab 100k -f webm -y output.webm
将input.mp4替换成你的输入文件,output.webm是输出文件名。注意在pass 1中ffmpeg将生成一个中间文件,因此两条命令应在同一目录下执行。

另外,如有必要,可以在命令行添加-s 1280x720这个参数,指定视频的尺寸,但我的实测表明这么做可能引起视频比例与原始视频不符。

参考资料:The p-Code Machine

2012年2月6日星期一

Unicode代码段和字符集对应表

0000-007F:C0控制符及基本拉丁文(C0ControlandBasicLatin)
0080-00FF:C1控制符及拉丁文补充-1(C1ControlandLatin1Supplement)
0100-017F:拉丁文扩展-A(LatinExtended-A)
0180-024F:拉丁文扩展-B(LatinExtended-B)
0250-02AF:国际音标扩展(IPAExtensions)
02B0-02FF:空白修饰字母(SpacingModifiers)
0300-036F:结合用读音符号(CombiningDiacriticsMarks)
0370-03FF:希腊文及科普特文(GreekandCoptic)
0400-04FF:西里尔字母(Cyrillic)
0500-052F:西里尔字母补充(CyrillicSupplement)
0530-058F:亚美尼亚语(Armenian)
0590-05FF:希伯来文(Hebrew)
0600-06FF:阿拉伯文(Arabic)
0700-074F:叙利亚文(Syriac)
0750-077F:阿拉伯文补充(ArabicSupplement)
0780-07BF:马尔代夫语(Thaana)
07C0-077F:西非书面语言(N'Ko)
0800-085F:阿维斯塔语及巴列维语(AvestanandPahlavi)
0860-087F:Mandaic
0880-08AF:撒马利亚语(Samaritan)
0900-097F:天城文书(Devanagari)
0980-09FF:孟加拉语(Bengali)
0A00-0A7F:锡克教文(Gurmukhi)
0A80-0AFF:古吉拉特文(Gujarati)
0B00-0B7F:奥里亚文(Oriya)
0B80-0BFF:泰米尔文(Tamil)
0C00-0C7F:泰卢固文(Telugu)
0C80-0CFF:卡纳达文(Kannada)
0D00-0D7F:德拉维族语(Malayalam)
0D80-0DFF:僧伽罗语(Sinhala)
0E00-0E7F:泰文(Thai)
0E80-0EFF:老挝文(Lao)
0F00-0FFF:藏文(Tibetan)
1000-109F:缅甸语(Myanmar)
10A0-10FF:格鲁吉亚语(Georgian)
1100-11FF:朝鲜文(HangulJamo)
1200-137F:埃塞俄比亚语(Ethiopic)
1380-139F:埃塞俄比亚语补充(EthiopicSupplement)
13A0-13FF:切罗基语(Cherokee)
1400-167F:统一加拿大土著语音节(UnifiedCanadianAboriginalSyllabics)
1680-169F:欧甘字母(Ogham)
16A0-16FF:如尼文(Runic)
1700-171F:塔加拉语(Tagalog)
1720-173F:Hanunóo
1740-175F:Buhid
1760-177F:Tagbanwa
1780-17FF:高棉语(Khmer)
1800-18AF:蒙古文(Mongolian)
18B0-18FF:Cham
1900-194F:Limbu
1950-197F:德宏泰语(TaiLe)
1980-19DF:新傣仂语(NewTaiLue)
19E0-19FF:高棉语记号(KmerSymbols)
1A00-1A1F:Buginese
1A20-1A5F:Batak
1A80-1AEF:Lanna
1B00-1B7F:巴厘语(Balinese)
1B80-1BB0:巽他语(Sundanese)
1BC0-1BFF:PahawhHmong
1C00-1C4F:雷布查语(Lepcha)
1C50-1C7F:OlChiki
1C80-1CDF:曼尼普尔语(Meithei/Manipuri)
1D00-1D7F:语音学扩展(PhoneticExtensions)
1D80-1DBF:语音学扩展补充(PhoneticExtensionsSupplement)
1DC0-1DFF:结合用读音符号补充(CombiningDiacriticsMarksSupplement)
1E00-1EFF:拉丁文扩充附加(LatinExtendedAdditional)
1F00-1FFF:希腊语扩充(GreekExtended)
2000-206F:常用标点(GeneralPunctuation)
2070-209F:上标及下标(SuperscriptsandSubscripts)
20A0-20CF:货币符号(CurrencySymbols)
20D0-20FF:组合用记号(CombiningDiacriticsMarksforSymbols)
2100-214F:字母式符号(LetterlikeSymbols)
2150-218F:数字形式(NumberForm)
2190-21FF:箭头(Arrows)
2200-22FF:数学运算符(MathematicalOperator)
2300-23FF:杂项工业符号(MiscellaneousTechnical)
2400-243F:控制图片(ControlPictures)
2440-245F:光学识别符(OpticalCharacterRecognition)
2460-24FF:封闭式字母数字(EnclosedAlphanumerics)
2500-257F:制表符(BoxDrawing)
2580-259F:方块元素(BlockElement)
25A0-25FF:几何图形(GeometricShapes)
2600-26FF:杂项符号(MiscellaneousSymbols)
2700-27BF:印刷符号(Dingbats)
27C0-27EF:杂项数学符号-A(MiscellaneousMathematicalSymbols-A)
27F0-27FF:追加箭头-A(SupplementalArrows-A)
2800-28FF:盲文点字模型(BraillePatterns)
2900-297F:追加箭头-B(SupplementalArrows-B)
2980-29FF:杂项数学符号-B(MiscellaneousMathematicalSymbols-B)
2A00-2AFF:追加数学运算符(SupplementalMathematicalOperator)
2B00-2BFF:杂项符号和箭头(MiscellaneousSymbolsandArrows)
2C00-2C5F:格拉哥里字母(Glagolitic)
2C60-2C7F:拉丁文扩展-C(LatinExtended-C)
2C80-2CFF:古埃及语(Coptic)
2D00-2D2F:格鲁吉亚语补充(GeorgianSupplement)
2D30-2D7F:提非纳文(Tifinagh)
2D80-2DDF:埃塞俄比亚语扩展(EthiopicExtended)
2E00-2E7F:追加标点(SupplementalPunctuation)
2E80-2EFF:CJK部首补充(CJKRadicalsSupplement)
2F00-2FDF:康熙字典部首(KangxiRadicals)
2FF0-2FFF:表意文字描述符(IdeographicDescriptionCharacters)
3000-303F:CJK符号和标点(CJKSymbolsandPunctuation)
3040-309F:日文平假名(Hiragana)
30A0-30FF:日文片假名(Katakana)
3100-312F:注音字母(Bopomofo)
3130-318F:朝鲜文兼容字母(HangulCompatibilityJamo)
3190-319F:象形字注释标志(Kanbun)
31A0-31BF:注音字母扩展(BopomofoExtended)
31C0-31EF:CJK笔画(CJKStrokes)
31F0-31FF:日文片假名语音扩展(KatakanaPhoneticExtensions)
3200-32FF:封闭式CJK文字和月份(EnclosedCJKLettersandMonths)
3300-33FF:CJK兼容(CJKCompatibility)
3400-4DBF:CJK统一表意符号扩展A(CJKUnifiedIdeographsExtensionA)
4DC0-4DFF:易经六十四卦符号(YijingHexagramsSymbols)
4E00-9FBF:CJK统一表意符号(CJKUnifiedIdeographs)
A000-A48F:彝文音节(YiSyllables)
A490-A4CF:彝文字根(YiRadicals)
A500-A61F:Vai
A660-A6FF:统一加拿大土著语音节补充(UnifiedCanadianAboriginalSyllabicsSupplement)
A700-A71F:声调修饰字母(ModifierToneLetters)
A720-A7FF:拉丁文扩展-D(LatinExtended-D)
A800-A82F:SylotiNagri
A840-A87F:八思巴字(Phags-pa)
A880-A8DF:Saurashtra
A900-A97F:爪哇语(Javanese)
A980-A9DF:Chakma
AA00-AA3F:VarangKshiti
AA40-AA6F:SorangSompeng
AA80-AADF:Newari
AB00-AB5F:越南傣语(Vi?tThái)
AB80-ABA0:KayahLi
AC00-D7AF:朝鲜文音节(HangulSyllables)
D800-DBFF:High-halfzoneofUTF-16
DC00-DFFF:Low-halfzoneofUTF-16
E000-F8FF:自行使用区域(PrivateUseZone)
F900-FAFF:CJK兼容象形文字(CJKCompatibilityIdeographs)
FB00-FB4F:字母表达形式(AlphabeticPresentationForm)
FB50-FDFF:阿拉伯表达形式A(ArabicPresentationForm-A)
FE00-FE0F:变量选择符(VariationSelector)
FE10-FE1F:竖排形式(VerticalForms)
FE20-FE2F:组合用半符号(CombiningHalfMarks)
FE30-FE4F:CJK兼容形式(CJKCompatibilityForms)
FE50-FE6F:小型变体形式(SmallFormVariants)
FE70-FEFF:阿拉伯表达形式B(ArabicPresentationForm-B)
FF00-FFEF:半型及全型形式(HalfwidthandFullwidthForm)
FFF0-FFFF:特殊(Specials)
10300..1032F;OldItalic
10330..1034F;Gothic
10400..1044F;Deseret
1D000..1D0FF;ByzantineMusicalSymbols
1D100..1D1FF;MusicalSymbols
1D400..1D7FF;MathematicalAlphanumericSymbols
20000..2A6D6;CJKUnifiedIdeographsExtensionB
2F800..2FA1F;CJKCompatibilityIdeographsSupplement
E0000..E007F;Tags
F0000..FFFFD;PrivateUse
100000..10FFFD;PrivateUse

2012年2月1日星期三

转贴:实现了一个比nginx速度更快的HTTP服务器


首先承认这个标题标题党了:)。在上次的FreeBSD和linux的nginx静态文件性能对比测试 后,我萌发了自己动手做一个简单的Web Server来搞清楚nginx高性能背后的原理的想法。最后成功实现了一个基于epoll的简单的HTTP服务器,实现了 200,404,400,304响应,并且性能比nginx高了一点点。本文主要介绍这个HTTP服务器的原理和设计过程。

阅读了一些文章(见最后的参考阅读)后,我整理出了以下要点:

实现多并发的socket服务器有这样几个方法:

1. 多进程共享一个监听端口

bind之后使用fork()创建一份当前进程的拷贝,并启动子进程。子进程采用阻塞式accept、read、write,即这些操作会阻塞线程,直到操作完成才继续执行。缺点是进程之间通信速度慢,每个进程占用很多内存,所以并发数一般受限于进程数。

2. 多线程

类似多进程,只不过用线程代替了进程。主线程负责accept,为每个请求建立一个线程(或者使用线程池复用线程)。比多进程速度快,占用更少的内存,稳定性不及多进程。因为每个线程都有自己的堆栈空间,其占用的内存还是无法免除的,所以并发数一般受限于线程数。

一个阻塞式IO程序的流程示例图:
QQ截图20110923131031

3. 事件驱动的非阻塞IO(nonblocking I/O)

单线程,将socket设置为非阻塞模式(accept、read、write会立即返回。如果已经accept完了所有的连接,或读光了缓冲区的 数据,或者写满了缓冲区,会返回-1,而不是进入阻塞状态)。使用select或epoll等机制,同时监听多个IO操作有无事件发生。当其中的一个或多 个处于Ready状态(即:监听的socket可以accept,tcp连接可以read等)后,立即处理相应的事件,处理完后立即回到监听状态(注意这 里的监听是监听IO事件,不是监听端口)。相当于阻塞式IO编程中任意一处都可能回到主循环中继续等待,并能从等待中直接回到原处继续执行;而 accept、读、写都不再阻塞,阻塞全部移动到了一个多事件监听操作中。

一个非阻塞式IO程序的流程示例图:

QQ截图20110923131039

举例来说,如果在A连接的Read request的过程中,缓冲区数据读完了,而请求还没有结束,直接返回到主循环中监听其它事件。而这时如果发现另一个Send了一半的Response 连接B变为了可写状态,则直接处理B连接Send Response事件,从上次B连接写了一半的地方开始,继续写入数据。这样一来,虽然是单线程,但A和B同时进行,互不干扰。

因为流程更加复杂,无法依靠线程的堆栈保存每个连接处理过程中的各种状态信息,我们需要自己维护它们,这种编程方式需要更高的技巧。比方说,原先我 们可以在send_response函数中用局部变量保存发送数据的进度,而现在我们只能找一块其它的地方,为每一个连接单独保存这个值了。

nginx即使用事件驱动的非阻塞IO模式工作。

nginx支持多种事件机制:跨平台的select,Linux的poll和epoll,FreeBSD的kqueue,Solaris的/dev/poll等。在高并发的情况下,在Linux上使用epoll性能最好,或者说select的性能太差了。

事件机制分为水平触发,或译状态触发(level-triggered)和边缘触发(edge-triggered)。前者是用通过状态表示有事件发生,后者通过状态变化表 示事件发生。打个比方来说,使用状态触发的时候,只要缓冲区有数据,你就能检测到事件的存在。而使用边缘触发,你必须把缓冲区的数据全部读完之后,才能进 行下一次事件的检测,否则,因为状态一直处于可读状态,没有发生变化,你将永远收不到这个事件。显然,后者对编写程序的严谨性要求更高。

select和poll属于前者,epoll同时支持这两种模式。值得一提的是,我自己测试了一下,发现即使在20000并发的情况下,epoll使用这两种模式之前性能差异仍可以忽略不计。

另外需要注意的是,对于常规文件设置非阻塞是不起作用的

4. 此外还有异步IO,一般在Windows上使用,这里就不谈了。

另外nginx使用了Linux的sendfile函数。和传统的用户程序自己read和write不同,sendfile接收两个文件描述符,直接在内核中实现复制操作,相比read和write,可以减少内核态和用户态的切换次数,以及数据拷贝的次数。

接下来正式开始设计。我选择了非阻塞IO,epoll的边缘触发模式。先找了个比较完整的使用epoll的一个socket server例子作为参考,然后在它的基础上边修改边做实验:

https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/

这个例子比较简单,而且也没有体现出非阻塞IO编程。不过通过它我了解到了epoll的基本使用方法。为了实现并发通信,我们需要把程序“摊平”。

首先,分析我们的HTTP服务器通信过程用到的变量:

状态 Wait for reading Wait for writing 次数 变量类型 非本地变量 备注
Accept Y N n local

Read request Y N n nonlocal Read buf
Open file N N n nonlocal 文件名
Send response header N Y n nonlocal Response header buf
Read file -> Send response content N Y n*n nonlocal Read&write buf
Write pos
fd
Sock
读满read buf或读到EOF,再发
发送时将read buf
Close file N N n
fd
Close socket N N n
sock

然后,定义一个结构用于保存这些变量:
struct process {
    int sock;
    int status;
    int response_code;
    int fd;
    int read_pos;
    int write_pos;
    int total_length;
    char buf[BUF_SIZE];
};

为了简便,我直接用一个全局数组装所有的process:
static struct process processes[MAX_PORCESS];

另外定义每个连接通信过程中的三个状态:
#define STATUS_READ_REQUEST_HEADER    0
#define STATUS_SEND_RESPONSE_HEADER    1
#define STATUS_SEND_RESPONSE        2

之后,就是按部就班地实现主循环、读取request,解析header,判断文件是否存在、检查文件修改时间,发送相应的header和content了。

下面只把程序中跟epoll有关的关键部分贴出来:

main()函数:

使用epoll_create()创建一个epoll fd,注意,这里的listen_sock已经设置为nonblocking(我使用了这篇文章中的setNonblocking函数)了:
    efd = epoll_create1 ( 0 );
    if ( efd == -1 )
    {
        ...
    }

    event.data.fd = listen_sock;
    event.events = EPOLLIN | EPOLLET;
    s = epoll_ctl ( efd, EPOLL_CTL_ADD, listen_sock, &event );
    if ( s == -1 )
    {
        ...
    }

    /* Buffer where events are returned */
    events = calloc ( MAXEVENTS, sizeof event );

这里的EPOLLIN表示监听“可读”事件。

在主循环中epoll_wait():
    while ( 1 )
    {
        int n, i;

        n = epoll_wait ( efd, events, MAXEVENTS, -1 );
        if ( n == -1 )
        {
            perror ( "epoll_wait" );
        }
        for ( i = 0; i < n; i++ )
        {
            if ( ( events[i].events & EPOLLERR ) ||
                    ( events[i].events & EPOLLHUP ) )
            {
                fprintf ( stderr, "epoll error\n" );
                close ( events[i].data.fd );
                continue;
            }

            handle_request ( events[i].data.fd );

        }
    }

epoll_wait()会在发生事件后停止阻塞,继续执行,并把发生了事件的event的file descriptor放入events中,返回数组大小。注意的是,这里要循环处理所有的fd。

接下来是关键部分:
void handle_request ( int sock )
{
    if ( sock == listen_sock )
    {
        accept_sock ( sock );
    }
    else
    {
        struct process* process = find_process_by_sock ( sock );
        if ( process != 0 )
        {
            switch ( process->status )
            {
            case STATUS_READ_REQUEST_HEADER:
                read_request ( process );
                break;
            case STATUS_SEND_RESPONSE_HEADER:
                send_response_header ( process );
                break;
            case STATUS_SEND_RESPONSE:
                send_response ( process );
                break;
            default:
                break;
            }
        }
    }
}

根据epoll返回的fd,做不同处理:如果是监听的socket,则accept();否则,根据sock的fd查找相应的process结构体,从中取回状态信息,返回到之前的处理状态中。这样就能实现信春哥,死后原地复活的状态恢复机制了。

在accept中,将accept出来的连接也设置为非阻塞,然后在process数组中找一个还没使用的空位,初始化,然后把这个socket存到process结构体中:
struct process* accept_sock ( int listen_sock )
{
    int s;
    // 在ET模式下必须循环accept到返回-1为止
    while ( 1 )
    {
        struct sockaddr in_addr;
        socklen_t in_len;
        int infd;
        char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
        if ( current_total_processes >= MAX_PORCESS )
        {
            // 请求已满,accept之后直接挂断
            infd = accept ( listen_sock, &in_addr, &in_len );
            if ( infd == -1 )
            {
                if ( ( errno == EAGAIN ) ||
                        ( errno == EWOULDBLOCK ) )
                {
                    break;
                }
                else
                {
                    perror ( "accept" );
                    break;
                }
            }
            close ( infd );

            return;
        }

        in_len = sizeof in_addr;
        infd = accept ( listen_sock, &in_addr, &in_len );
        if ( infd == -1 )
        {
            if ( ( errno == EAGAIN ) ||
                    ( errno == EWOULDBLOCK ) )
            {
                break;
            }
            else
            {
                perror ( "accept" );
                break;
            }
        }

        getnameinfo ( &in_addr, in_len,
                      hbuf, sizeof hbuf,
                      sbuf, sizeof sbuf,
                      NI_NUMERICHOST | NI_NUMERICSERV );

        //设置为非阻塞        s = setNonblocking ( infd );
        if ( s == -1 )
            abort ();
        int on = 1;
        setsockopt ( infd, SOL_TCP, TCP_CORK, &on, sizeof ( on ) );
        //添加监视sock的读取状态
        event.data.fd = infd;
        event.events = EPOLLIN | EPOLLET;
        s = epoll_ctl ( efd, EPOLL_CTL_ADD, infd, &event );
        if ( s == -1 )
        {
            perror ( "epoll_ctl" );
            abort ();
        }
        struct process* process = find_empty_process_for_sock ( infd );
        current_total_processes++;
        reset_process ( process );
        process->sock = infd;
        process->fd = NO_FILE;
        process->status = STATUS_READ_REQUEST_HEADER;
    }
}

三个不同状态对应三个不同函数进行处理,我就不全贴了,以read_request为例:
void read_request ( struct process* process )
{
    int sock = process->sock, s;
    char* buf=process->buf;
    char read_complete = 0;

    ssize_t count;

    while ( 1 )
    {
        count = read ( sock, buf + process->read_pos, BUF_SIZE - process->read_pos );
        if ( count == -1 )
        {
            if ( errno != EAGAIN )
            {
                handle_error ( process, "read request" );
                return;
            }
            else
            {
                //errno == EAGAIN表示读取完毕
                break;
            }
        }
        else if ( count == 0 )
        {
            // 被客户端关闭连接
            cleanup ( process );
            return;
        }
        else if ( count > 0 )
        {
            process->read_pos += count;
        }
    }

    int header_length = process->read_pos;
    // determine whether the request is complete
    if ( header_length > BUF_SIZE - 1 )
    {
    process->response_code = 400;
    process->status = STATUS_SEND_RESPONSE_HEADER;
    strcpy ( process->buf, header_400 );
    send_response_header ( process );
    handle_error ( processes, "bad request" );
    return;
    }
    buf[header_length]=0;
    read_complete = ( strstr ( buf, "\n\n" ) != 0 ) || ( strstr ( buf, "\r\n\r\n" ) != 0 );

    if ( read_complete )
    {
        // ...
        //解析之后,打开文件,把文件描述符存入process,然后进入发送header状态 
        process->status = STATUS_SEND_RESPONSE_HEADER;
        //修改此sock的监听状态,改为监视写状态
        event.data.fd = process->sock;
        event.events = EPOLLOUT | EPOLLET;
        s = epoll_ctl ( efd, EPOLL_CTL_MOD, process->sock, &event );
        if ( s == -1 )
        {
            perror ( "epoll_ctl" );
            abort ();
        }
        //发送header
        send_response_header ( process );
    }
}

这里的注意点如下:

1. 读取的时候要一直循环读取到返回-1为止,然后检查errno,如果errno为EAGAIN,表示缓冲区已经空了,这个socket变为了“不可读”。如果不读完,边缘触发模式的epoll_wait将永远不会再触发这个socket的“可读”事件。

2. 使用epoll_ctl ( efd, EPOLL_CTL_MOD, process->sock, &event )修改epoll的状态,这里在读完后,我们要继续监听“可写”事件,因此要把epoll监听的事件改为EPOLLOUT。

接下来不断完善这个程序并进行优化,并实现了304 not modified功能之后,用ab测试性能,并和nginx对比:
Server Software:        clowwindyserver/1.0
Server Hostname:        localhost
Server Port:            8082
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      100
Time taken for tests:   2.241 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574420000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4462.88 [#/sec] (mean)
Time per request:       22.407 [ms] (mean)
Time per request:       0.224 [ms] (mean, across all concurrent requests)
Transfer rate:          250348.23 [Kbytes/sec] received
Server Software:        nginx/0.7.67
Server Hostname:        localhost
Server Port:            80
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      100
Time taken for tests:   2.490 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574720000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4016.54 [#/sec] (mean)
Time per request:       24.897 [ms] (mean)
Time per request:       0.249 [ms] (mean, across all concurrent requests)
Transfer rate:          225428.04 [Kbytes/sec] received

结果很令人欣慰的比nginx快了一点点,并且只用了700K内存。不过作为一个功能比nginx少了很多的程序来说这一结果是意料之中的。

然后试图测试上万并发的情况,结果too many open files了。于是修改fd数限制:
# echo 32768 > /proc/sys/fs/file-max
# ulimit -n 32768

再次测试:
Server Software:        clowwindyserver/1.0
Server Hostname:        localhost
Server Port:            8082
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      10000
Time taken for tests:   2.249 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574420000 bytes
HTML transferred:       572440000 bytes
Requests per second:    4445.59 [#/sec] (mean)
Time per request:       2249.420 [ms] (mean)
Time per request:       0.225 [ms] (mean, across all concurrent requests)
Transfer rate:          249378.52 [Kbytes/sec] received

nginx设置worker_connections  20480以后:
Server Software:        nginx/0.7.67
Server Hostname:        localhost
Server Port:            80
Document Path:          /jquery.js
Document Length:        57244 bytes
Concurrency Level:      10000
Time taken for tests:   2.715 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      574720000 bytes
HTML transferred:       572440000 bytes
Requests per second:    3683.83 [#/sec] (mean)
Time per request:       2714.569 [ms] (mean)
Time per request:       0.271 [ms] (mean, across all concurrent requests)
Transfer rate:          206754.74 [Kbytes/sec] received

结果性能只下降了一点点,并且依然领先nginx。不过,为了承受更多的连接调大process数组以后,进程一共吃了40M内存。而 nginx仅用了5M内存。因为我们使用了极其浪费内存的用数组装连接状态和缓存的方法,所以会吃掉缓存大小(process.buf的大小)乘以 process数组的大小的内存。调小缓存以后内存占用降到6M,不过这并不是根本解决之道,还是存在很大的浪费。如果改为动态内存管理,应该就会小于 nginx了。

这说明事件驱动的非阻塞IO可以顶得住上万并发,并需要远小于阻塞式编程的服务器程序的内存,速度也更快。

最后把源码丢到github了,想看完整源码的同学请移步:

https://github.com/clowwindy/clowwindy_server
参考阅读:

The C10K problem (强烈推荐)

Introduction to non-blocking I/O

Non-blocking I/O with regular files

Linux Files and the Event Poll Interface
 

2012年1月20日星期五

12306性能问题之我见

铁道部12306订票网站可谓是生不逢时。在春运的大潮下,没能经受住考验。既然是网络订票,其使用的主体必然是广大的“网民”,其中不乏身手不凡的“极客”和专家。继令人叹为观止的使用Firebug抢火车票(不知农民工兄弟们会怎么想)以后,又有人本着“得一火车票胜造七级浮屠”的仁慈之心,开发出了”造福万民“的12306 Helper火狐插件。这些极客手段和插件的诞生对12306来说更是雪上加霜。

要解决这个问题也不是不可能,网上除了上述这些损人利己的极客手段外,还涌现出很多为12306”支招“的文章,其中有很多关于大容量web站点架构的思想值得我们去学习和思考。比如酷壳风云这里。作为一个研究网站架构技术的从业人员,我也谈一下这个问题。 

大型购票系统的设计

用过12306.cn的人,尤其是互联网从业人员,均会觉得这个网站很“烂”。依我之见,烂体现在两个方面,一个是界面以及使用体验之烂;另一个是架构理念上的烂 -- 仍然是在用N年前做ERP的思路在做一个web 2.0时代大型的电子商务网站。第一点是顾客能够感受到的,而第二点则不是那么明显了。

那么,这个系统正确的做法是什么呢?上文引用的几篇博文无一例外地为铁道部指明了正确的方向:使用队列系统。我完全赞同他们的思路,只是在实现的细节上有我自己的思考。

使用流程

我设想的购票流程是这样的:

1. 车次查询

客户输入所需的出发地、目的地和出发日期/时间查询列车班次。 在这一步骤中,只有出发和目的地是关键信息。客户可以一次性按照他自己的优先次序选择多个不同日期/时间出发的车次,全部加入“购物篮”。

2. 订单确认和预付款
 
在订单确认环节,客户输入乘车人的身份证件信息,提交订单。系统将首先验证用户输入的手机号确为客户所拥有(发送验证码),然后引导客户进行网上支付。注意:在此步骤中支付的款项为预付款,即押金,支付并不代表已经成功购得车票。在支付之前,系统会请用户确认若购票不成功,押金何时返还。客户可以选择押金在操作后的第二天立即返还,也可以选择押金在若干天后如仍无法成功购得车票才返还。支付以后,系统给出一个6~8位字符的确认码,通过短信发送到客户的手机上。

3. 竞购

系统后台建立一个队列,分下单和订单处理两个阶段运作。例如,设定每个周期为20分钟,其中每个周期的后10分钟为订单处理阶段。没能在周期前10分钟内完成押金支付的客户的订单将累积到下个周期的订单处理阶段中处理。每周期的竞购结束后,若客户购票成功,系统将发送一条短信至客户手机,此短信包含在订单确认环节中生成的确认码;若客户购票不成功,订单将被自动累积到下一个处理周期,同时,若此轮为该客户第一次参与竞购,系统也将发送一条提示短信(后续周期中如果该客户仍然未成功购得车票,系统将不再发送短信提示)。

4. 定位和取票

竞购成功的客户可以凭手机号码(或身份证号码)以及确认码登录取票网站选择自己的座位,也可以在代售点或车站进行座位确认,并取票,取票时需使用身份证。

5. 退款

发生退款的情形有三种:
  • 未成功购得车票,这里又分两种原因:
    • 身份信息不正确(如:姓名和身份证号不匹配):押金将退回到付款所用的银行账户
    • 竞购失败:押金将退回到乘车人所拥有的银行账户
  • 退票:押金将退回到乘车人所拥有的银行账户 
退款操作系统自动执行,无需用户或运营管理人员干预。

6. 查询及退票

客户可以通过专门的网站查询界面通过手机号、身份证号以及确认码来查询订票情况、执行退票操作(包括输入退票所用的账户信息)。

为什么这样设计

铁路售票系统最需要解决的问题就是:在僧多粥少的情况下,如何公平地分配稀缺资源?引申出来的问题是:如何设计一个高效的在线售票系统应对大流量?如何防范黄牛或恶意攻击?在流程设计层面,我的设计方案做到了两点:

有效而且公平

与本文开头所引用的几篇博文的一个小区别是,我在设计队列系统的时候特别考虑了所谓“秒杀”的做法,并在流程设计上加以杜绝。同时,方案中又保留了队列系统的基本特征,即先来后到。

鲁棒而且安全

本系统最大的特色是免注册、免登录(查询除外)。通过简化流程和技术手段的配合,可以极大地提升系统的吞吐量,而又不降低系统的鲁棒性和安全性:
  1. 充分利用车票实名制,完全避免注册和登录过程。
  2. 通过预付款方式,避免黄牛、恶意攻击者和“秒杀”者对服务器产生过大压力。
  3. 通过退款账户限制,从根本上消灭黄牛的生存空间。同时,由于使用了完全异步化的队列系统和唯一的编号(身份证),系统无需想方设法去防止“占着茅坑不拉屎”(或者说DOS攻击)或者重复不合理购买(比如一人购买同一天的多个车次等),这样也就避免了“错杀”(即一个心怀不轨的人乱输入别人的身份证号码,导致真正想购票的人无法购票),提高了用户体验。
设计上的要点

我自信这套购票流程设计是比较完善的(当然,如果你发现了问题或有所欠缺,欢迎留言或通过email和我联系)。但它也不是没有问题的。在设计上,它的主要问题就是和外部系统的互通上。它需要和公安系统联系,验证身份信息的有效性(验证是后台异步进行的,这样可以避免流量压力,也可以保护公民身份信息);还需要和银行支付系统联系,尤其是退款通道上是否会有由于银行系统规范而导致的一些问题,我不得而知。

但与外部系统沟通这个问题现有的12306网站也是要做的,而且我相信这些事情对于铁道部来说是小菜一碟了。这并不是本文所要讨论的内容,不展开了。

技术上的要点

客户端查询

导致当前系统的压力过大的主要原因是购票过程中必不可少的查询,包括车次的查询和剩余票量的查询。我的解决方案是:车次查询完全在客户端完成;不提供实时的剩余票量查询。

具体的解决方案是,客户告诉服务器他的出发和到达地点,凭这点信息,服务器立即返回一套这两个地点之间的车次的所有数据,包括车次、时间、是否有票或剩余票量(不区分车型是否动车、不区分是否始发站,所有数据一次性返回)。这些数据以gzip形式压缩存储于内存数据库(如redis)中,web服务器返回此类数据的速度可以超过读取磁盘文件的速度。客户端取得数据后,以javascript查询本地数据,往“购物篮”中添加订单,直到订单提交时才会再次与服务器沟通。 注意:剩余票量在开始取回以后不实时更新,直到下一个竞购周期才会刷新。

通过这种手段,服务器端的链接数和PV可以至少有数量级以上的下降。用这里提到的数据估算,12306的峰值访问量可能达到两个小时5亿PV,也就是说,每秒钟大约7万个hit。假设新方案降低PV到原来的1/10,也就是不超过7000hits/s。这种量也就一台NginX就可以搞定。当然,实际可能还有HA、热备但也不至于太复杂了。而查询所需的基于JavaScript技术的“列车时刻表”,我没有测算过全国有多少个车站,又有多少种换乘方案,随便估算一下,弄两台128G内存的服务器做Master-Slave,肯定可以搞定了(其实不会有那么多,因为只有热数据才会保存在内存中)。

按功能集群

这个问题我引用的博文中被多次提到,也是架构设计中需注意的“最佳实践”之一。例如,订票网站叫做ticket.12306.cn,提供查询功能,下单服务器是order.12306.cn,而后台还有一堆集群负责订单的处理,这才是本系统工作量集中的地方。后台订单处理系统的任务分配可以根据出发地和到达地进行合理的聚合,即同一(或近似)路线的放在同一服务器上处理。

恰当的防DDOS系统

为了提供最鲁棒的系统,我认为没有必要限制每个用户每次能购买多少票,在设计中也做了种种考虑来实现这种“完全开放”的下单方式。但在IT层面,恰当的防止攻击还是需要的,比如防止同一个IP过快地访问服务器等等。

吾道一以贯之

谈到架构,很多人可能认为是“后端” 的问题,脑子里面浮现出来的可能是CISCO的路由器、IBM的服务器、NginX等等。经验丰富的工程师同时会考虑前端的优化,比如使用AJAX技术、 页面静态化、用YSlow去分析一下等等。但较少有人会将架构和产品设计联系在一起。其实,“架构”是贯穿互联网产品方方面面的灵魂。一个好的架构可以令 你的产品光芒四射,而一个坏的产品设计则会让架构师伤透脑筋,让耗资不菲的硬件设备不堪重负。

上面,我从12306这个活生生的例子阐述了在“架构”这个问题上我的思考:架构问题是一个从需求分析到产品设计到技术实现的大问题。一个优秀的架构师不仅仅要懂得技术实现的手段,而且还必须是一个合格的需求分析人员和产品设计师。