fastText 源码分析
介绍
fastText 是 facebook 近期开源的一个词向量计算以及文本分类工具,该工具的理论基础是以下两篇论文:
这篇论文提出了用 word n-gram 的向量之和来代替简单的词向量的方法,以解决简单 word2vec 无法处理同一词的不同形态的问题。fastText 中提供了 maxn 这个参数来确定 word n-gram 的 n 的大小。 Continue reading
fastText 是 facebook 近期开源的一个词向量计算以及文本分类工具,该工具的理论基础是以下两篇论文:
这篇论文提出了用 word n-gram 的向量之和来代替简单的词向量的方法,以解决简单 word2vec 无法处理同一词的不同形态的问题。fastText 中提供了 maxn 这个参数来确定 word n-gram 的 n 的大小。 Continue reading
近一年各种深度学习平台和硬件层出不穷,各种xPU的功耗和面积数据也是满天飞,感觉有点乱。在这里我把我看到的一点情况做一些小结,顺便列一下可能的市场。在展开之前,我想强调的是,深度学习的应用无数,我能看到的只有能在千万级以上的设备中部署的市场,各个小众市场并不在列。
深度学习目前最能落地的应用有两个方向,一个是图像识别,一个是语音识别。这两个应用可以在如下市场看到:个人终端(手机,平板),监控,家庭,汽车,机器人,服务器。
先说手机和平板。这个市场一年的出货量在30亿颗左右(含功能机),除苹果外总值300亿刀。手机主要玩家是苹果(3亿颗以下),高通(8亿颗以上),联发科(7亿颗以上),三星(一亿颗以下),海思(一亿颗),展讯(6亿颗以上),平板总共4亿颗左右。而28纳米工艺,量很大的话(1亿颗以上),工程费用可以摊的很低,平均1平方毫米的成本是8美分左右,低端4G芯片(4核)的面积差不多是50平方毫米以下,成本就是4刀。中端芯片(8核)一般在100平方毫米左右,成本8刀。16纳米以及往上,同样的晶体管数,单位成本会到1.5倍。一般来说,手机的物料成本中,处理器芯片(含基带)价格占了1/6左右。一个物料成本90刀的手机,用的处理器一般在15刀以下,甚至只有10刀。这个10刀的芯片,包含了处理器,图形处理器,基带,图像信号处理器,每一样都是高科技的结晶,却和肯德基全家桶一个价,真是有点惨淡。然而生产成本只是一部分,人力也是很大的开销。一颗智能机芯片,软硬开发,测试,生产,就算全用的成熟IP,也不会少于300人,每人算10万刀的开销,量产周期两年,需要6000万刀。外加各种EDA工具,IP授权和开片费,芯片还没影子,1亿刀就下去了。
言归正传,手机上的应用,最直接的就是美颜相机,AR和语音助手。这些需求翻译成硬件指令就是对8位整数点乘(INT8)和16位浮点运算(FP16)的支持。具体怎么支持?曾经看到过一张图,我觉得较好的诠释了这一点:
智能手机和平板上,是安卓的天下,所有独立芯片商都必须跟着谷歌爸爸走。谷歌已经定义了Android NN作为上层接口,可以支持它的TensorFlow以及专为移动设备定义的TensorFlow Lite。而下层,针对各种不同场景,可以是CPU,GPU,DSP,也可以是硬件加速器。它们的能效比如下图:
可以看到,在TSMC16纳米工艺下,大核能效比是10-100Gops/W(INT8),小核可以做到100G-1Tops/W,手机GPU是300Gops/W,而要做到1Tops/W以上,必须使用加速器。这里要指出的是,小核前端设计思想与大核完全不同,在后端实现上也使用不同的物理单元,所以看上去和大核的频率只差50%,但是在逻辑运算能效比上会差4倍以上,在向量计算中差的就更多了。
手机的长时间运行场景下,芯片整体功耗必须小于2.5瓦,分给深度学习任务的,不会超过1.5瓦。相对应的,如果做到1Tops/W,那这就是1.5T(INT8)的处理能力。对于照片识别而言,情况要好些,虽然对因为通常不需要长时间连续的处理。这时候,CPU是可以爆发然后休息的。语音识别对性能要求比较低,100Gops可以应付一般应用,用小核也足够。但有些连续的场景,比如AR环境识别,每秒会有30-60帧的图像送进来,如果不利用前后文帮助判断,CPU是没法处理的。此时,就需要GPU或者加速器上场。
上图是NVidia的神经网络加速器DLA,它只有Inference的功能。前面提到在手机上的应用,也只需要Inference来做识别,训练可以在服务端预先处理,训练好的数据下载到手机就行,识别的时候无需连接到服务端。
DLA绿色的模块形成类似于固定的流水线,上面有一个控制模块,可以用于动态分配计算单元,以适应不同的网络。我看到的大多数加速器其实都是和它大同小异,除了这了几百K字节的SRAM来存放输入和权值(一个273×128, 128×128, 128×128 ,128×6 的4层INT8网络,需要70KBSRAM)外,而有些加速器增加了一个SmartDMA引擎,可以通过简单计算预取所需的数据。根据我看到的一些跑分测试,这个预取模块可以把计算单元的利用率提高到90%以上。
至于能效比,我看过的加速器,在支持INT8的算法下,可以做到1.2Tops/W (1Ghz@T16FFC),1Tops/mm^2,并且正在向1.5Tops/W靠近。也就是说,1.5W可以获得2Tops(INT8)的理论计算能力。这个计算能力有多强呢?我这目前处理1080p60FPS的图像中的60×60及以上的像素大小的人脸识别,大致需要0.5Tops的计算能力,2Tops完全可以满足。当然,如果要识别复杂场景,那肯定是计算力越高越好。
为什么固定流水的能效比能做的高?ASIC的能效比远高于通用处理器已经是一个常识,更具体一些,DLA不需要指令解码,不需要指令预测,不需要乱序执行,流水线不容易因为等待数据而停顿。下图是某小核各个模块的动态功耗分布,计算单元只占1/3,而指令和缓存访问占了一半。
但是移动端仅仅有神经网络加速器是远远不够的。比如要做到下图效果,那首先要把人体的各个细微部位精确识别,然后用各种图像算法来打磨。而目前主流图像算法和深度学习没有关系,也没看到哪个嵌入式平台上的加速器在软件上有很好的支持。目前图像算法的支持平台还主要是PC和DSP,连嵌入式GPU做的都一般。
那这个问题怎么解决?我看到两种思路:
第一种,GPU内置加速器。下图是Verisilicon的Vivante改的加速器,支持固定流水的加速器和可编程模块Vision core(类似GPU中的着色器单元),模块数目可配,可以同时支持视觉和深度学习算法。不过在这里,传统的图形单元被砍掉了,以节省功耗和面积。只留下调度器等共用单元,来做异构计算的调度。
这类加速器比较适合于低端手机,自带的GPU和CPU本身并不强,可能光支持1080p的UI就已经耗尽GPU资源了,需要额外的硬件模块来完成有一定性能需求的任务。
对于中高端手机,GPU和CPU的资源在不打游戏的时候有冗余,那么就没有必要去掉图形功能,直接在GPU里面加深度学习加速器就可以,让GPU调度器统一调度,进行异构计算。
上图是某款GPU的材质计算单元,你有没有发现,其实它和神经网络加速器的流水线非常类似?都需要权值,都需要输入,都需要FP16和整数计算,还有数据压缩。所不同的是计算单元的密度,还有池化和激活。稍作改动,完全可以兼容,从而进一步节省面积。
但是话说回来,据我了解,目前安卓手机上各种图像,视频和视觉的应用,80%其实都是用CPU在处理。而谷歌的Android NN,默认也是调用CPU汇编。当然,手机芯片自带的ISP及其后处理,由于和芯片绑的很紧,还是能把专用硬件调动起来的。而目前的各类加速器,GPU,DSP,要想和应用真正结合,还有挺长的路要走。
终端设备上还有一个应用,AR。据说iPhone8会实现这个功能,如果是的话,那么估计继2015的VR/AR,2016的DL,2017的NB-IOT之后,2018年又要回锅炒这个了。
那AR到底用到哪些技术?我了解的如下,先是用深度传感器得到场景深度信息,然后结合摄像头拍到的2维场景,针对某些特定目标(比如桌子,面部)构建出一个真实世界的三维物体。这其中需要用到图像识别来帮助判断物体,还需要确定物体边界。有了真实物体的三维坐标,就可以把所需要渲染的虚拟对象,贴在真实物体上。然后再把摄像头拍到的整个场景作为材质,贴到背景图层,最后把所有这些图层输出到GPU或者硬件合成器,合成最终输出。这其中还需要判断光源,把光照计算渲染到虚拟物体上。这里每一步的计算量有多大?
首先是深度信息计算。获取深度信息目前有三个方法,双目摄像头,结构光传感器还有TOF。他们分别是根据光学图像差异,编码后的红外光模板和反射模板差异,以及光脉冲飞行时间来的得到深度信息。第一个的缺点是需要两个摄像头之间有一定距离并且对室内光线亮度有要求,第二个需要大量计算并且室外效果不佳,第三个方案镜头成本较高。据说苹果会用结构光方案,主要场景是室内,避免了缺点。结构光传感器的成本在2-3刀之间,也是可以接受的。而对于计算力的要求,最基本的是对比两个经过伪随机编码处理过的发射模板以及接受模板,计算出长度差,然后用矩阵倒推平移距离,从而得到深度信息。这可以用专用模块来处理,我看到单芯片的解决方案,720p60FPS的处理能力,需要20GFLOPS FP32的计算量以上。换成CPU,就是8核。当然,我们完全可以先识别出目标物体,用图像算法计算出轮廓,还可以降低深度图的精度(通常不需要很精确),从而大大降低计算量。而识别本身的计算量前文已经给出,计算轮廓是经典的图像处理手段,针对特定区域的话计算量非常小,1-2个核就可以搞定。
接下去是根据深度图,计算真实物体的三维坐标,并输出给GPU。这个其实就是GPU渲染的第一阶段的工作,称作顶点计算。在移动设备上,这部分通常只占GPU总计算量的10%,后面的像素计算才是大头。产生虚拟物体的坐标也在这块,同样也很轻松。
接下去是生成背景材质,包括产生minimap等。这个也很快,没什么计算量,把摄像头传过来的原始图像放到内存,告诉GPU就行。
稍微麻烦一些的是计算虚拟物体的光照。背景贴图的光照不需要计算,使用原图中的就可以。而虚拟物体需要从背景贴图抽取亮度和物体方向,还要计算光源方向。我还没有见过好的算法,不过有个取巧,就是生成一个光源,给一定角度从上往下照,如果对AR要求不高也凑合了。
其他的渲染部分,和VR有些类似,什么ATW啊,Front Buffer啊,都可以用上,但是不用也没事,毕竟不是4K120FPS的要求。总之,AR如果做的不那么复杂,对CPU和GPU的性能要求并不高,搞个图像识别模块,再多1-2个核做别的足矣。
有了计算量,深度学习加速器对于带宽的需求是多少?如果SRAM足够大,1Tops的计算量需要5GB/s以下的带宽。连接方法可以放到CPU的加速口ACP(跑在1.8GHz的ARMv8.2内部总线可以提供9GB/s带宽)。只用一次的数据可以设成非共享类型,需要和CPU交换或者常用的数据使用Cacheable和Shareable类型,既可以在三级缓存分配空间,还可以更高效的做监听操作,免掉刷缓存。
不过,上述前提成立的前提是权值可以全部放到SRAM或者缓存。对于1TOPS INT8的计算量,所需权值的大小是512GB。如果全部放DDR,由于手机的带宽最多也就是30GB/S,是完全不够看的。对于输入,中间值和输出数据,我在上文有个例子,一个273×128, 128×128, 128×128 ,128×6 的4层INT8网络,需要70KB的SRAM(片内)放权值,共7万个。但是输入,输出和中间结果加起来却只有535个,相对来说并不大。这里的运算量是14万次(乘和加算2次)。对于1T的运算量来说,类似。中间数据放寄存器,输出数据无关延迟,只看带宽,也够。最麻烦的就是权值,数据量大到带宽无法接受。我看到的有些深度学习的算法,权值在几十到200兆,这样无论如何是塞不进SRAM的。哪怕只有10%需要读入,那也是50GB/s的带宽。我觉得,现阶段宣传的各种漂亮的跑分和图像识别速度,一旦权值太大,一定会让计算单元利用率大大下降。虽说现在有压缩算法压缩稀疏矩阵,也有人号称有几十倍的压缩率,但对于繁杂的各类应用,可能最后实际效果没比CPU好到哪里去。
如果加速器在GPU上,那么还是得用传统的ACE口,一方面提高带宽,一方面与GPU的核交换数据在内部进行,当然,与CPU的交互必然会慢一些。
在使用安卓的终端设备上,深度学习可以用CPU/DSP/GPU,也可以是加速器,但不管用哪个,一定要跟紧谷歌爸爸。谷歌以后会使用Vulkan Compute来替代OpenCL,使用Vulkan 来替代OpenGL ES,做安卓GPU开发的同学可以早点开始熟悉了。
高通推过用手机做训练,然后手机间组网,形成强大的计算力。从我的角度看,这个想法问题多多,先不说实际应用,谁会没事开放手机给别人训练用?耗电根本就吃不消。并且,要是我知道手机偷偷的上传我的图像和语音模板到别人那里,绝对不会买。
第二个市场是家庭,包括机顶盒/家庭网关(4亿颗以下),数字电视(3亿颗以下),电视盒子(1亿以下)三大块。整个市场出货量在7亿片,电器里面的MCU并没有计算在内。这个市场公司比较散,MStar/海思/博通/Marvell/Amlogic都在里面,小公司更是无数。如果没有特殊要求,拿平板的芯片配个wifi就可以用。当然,中高端的对画质还是有要求,MTK现在的利润从手机移到了电视芯片,屏幕显示这块有独到的技术。很多机顶盒的网络连接也不是以太网,而是同轴电缆等,这种场合也得专门的芯片。最近,这个市场里又多了一个智能音箱,各大互联网公司又拿出当年追求手机入口的热情来布局,好不热闹。
家庭电子设备里还有一个成员,游戏机。Xbox和PS每年出货量均在千万级别。VR/AR和人体识别早已经用在其中。
对于语音设别,100Gops的性能需求对于无风扇设计引入的3瓦功耗限制,CPU/DSP和加速器都可以选。不过工艺就得用28纳米了或者更早的了,毕竟没那么多量,撑不起16纳米。最便宜的方案,可以使用RISC-V+DLA,没有生态系统绑定的情况下最省成本。独立的加速器本身对CPU要求并不高,不像GPU那样需要支持OpenCL/OpenGL ES。8核G71@900Mhz差不多需要一个2GHz的A73来支持。并且由于OpenGL ES的限制,还不能使用小核来分担任务。而100Gops的语音处理我估计几百兆赫兹的处理器就可以了。
图像方面的应用,主要还是人脸识别和播放内容识别,不过这还没有成为一个硬需求。之前提过,0.5Tops足以搞定简单场景,4K分辨率的话,性能需求是1080p的四倍。
接下去是监控市场。监控市场上的图像识别是迄今为止深度学习最硬的需求。监控芯片市场本身并不大,有1亿颗以上的量,销售额20亿刀左右。主流公司有安霸,德州仪器和海思,外加几个小公司,OEM自己做芯片的也有。
传统的监控芯片数据流如上图蓝色部分,从传感器进来,经过图像信号处理单元,然后送给视频编码器编码,最后从网络输出。如果要对图像内容进行识别,那可以从传感器直接拿原始数据,或者从ISP拿处理过的图像,然后进行识别。中高端的监控芯片中还会有个DSP,做一些后处理和识别的工作。现在深度学习加速器进来,其实和DSP是有些冲突的。以前的一些经典应用,比如车牌识别等,DSP其实就已经做得很好了。如果要做识别以外的一些图像算法,这颗DSP还是得在通路上,并不能被替代。并且,DSP对传统算法的软件库支持要好得多。这样,DSP替换不掉,额外增加处理单元在成本上就是一个问题。
对于某些低功耗的场景,我看到有人在走另外一条路。那就是完全扔掉DSP,放弃存储和传输视频及图像,加入加速器,只把特征信息和数据通过NB-IOT上传。这样整个芯片功耗可以控制在500毫瓦之下。整个系统结合传感器,只在探测到有物体经过的时候打开,平时都处于几毫瓦的待机状态。在供电上,采用太阳能电池,100mmx100mm的面板,输出功率可以有几瓦。不过这个产品目前应用领域还很小众。
做识别的另一个途径是在局端。如果用显卡做,GFX1080的FP32 GLOPS是9T,180瓦,1.7Ghz,16纳米,320mm。而一个Mali G72MP32提供1T FP32的GFLOPS,16纳米,850Mhz,8瓦,9T的话就是72瓦,666mm。当然,如果G72设计成跑在1.7Ghz,我相信不会比180瓦低。此外桌面GPU由于是Immediate rendering的,带宽大,但对缓存没有很大需求,所以移动端的GPU面积反而大很多,但相对的,它对于带宽需求小很多,相应的功耗少很多
GPU是拿来做训练的,而视频识别只需要做Inference,如果用固定流水的加速器,按照NVIDIA Tesla P40的数据,48T INT8 TOPS,使用固定流水加速器,在16nm上只需要48mm。48Tops对应的识别能力是96路1080p60fps,96路1080p60fps视频解码器对应的面积差不多是 50mm,加上SRAM啥的,估计200mm以下。如果有一千万的量,那芯片成本可以做到40美金以下(假定良率还可以,不然路数得设计的小一点),而一块Tesla P40板子的售价是500美金(包括DDR颗粒),还算暴利。国内现在不少小公司拿到了投资在做这块的芯片。
第四个市场是机器人/无人机。机器人本身有多少量我没有数据,手机和平板的芯片也能用在这个领域。无人机的话全球一年在200万左右,做视觉处理的芯片也应该是这个量级。。用到的识别模块目前看还是DSP和CPU为主,因为DSP还可以做很多图像算法,和监控类似。这个市场对于ISP和深度信息的需求较高,双摄和结构光都可以用来算深度计算,上文提过就不再展开。
在无人机上做ISP和视觉处理,除了要更高的清晰度和实时性外,还比消费电子多了一个要求,容错。无人机的定位都靠视觉,如果给出的数据错误或者模块无反应都不符合预期。解决这个问题很简单,一是增加各种片内存储的ECC和内建自检,二是设两个同样功能的模块,错开时钟输入以避免时钟信号引起的问题,然后输出再等相同周期,同步到一个时钟。如果两个结果不一致,那就做特殊处理,避免扩散数据错误。
第五个市场是汽车,整个汽车芯片市场近300亿刀,玩家众多:
在汽车电子上,深度学习的应用就是ADAS了。在ADAS里面,语音和视觉从技术角度和前几个市场差别不大,只是容错这个需求进一步系统化,形成Function Safety,整个软硬件系统都需要过认证,才容易卖到前装市场。Function Safety比之前的ECC/BIST/Lock Step更进一步,需要对整个芯片和系统软件提供详细的测试代码和文档,分析在各类场景下的错误处理机制,连编译器都需要过认证。认证本身分为ASIL到A-ASIL-D四个等级,最高等级要求系统错误率小于1%。我对于这个认证并不清楚,不过国内很多手机和平板芯片用于后装市场的ADAS,提供语音报警,出货量也是过百万的。
最后放一张ARM的ADAS参考设计框图。
可能不会有人照着这个去设计ADAS芯片,不过有几处可以借鉴:
右方是安全岛,内涵Lock Step的双Cortex-R52,这是为了能够保证在左边所有模块失效的情况下复位整个系统或者进行异常中断处理的。中部蓝色和绿色的CryptoCell模块是对整个系统运行的数据进行保护,防止恶意窃取的。关于Trustzone设计我以前的文章有完整介绍这里就不展开了。
以上几个市场基本都是Inference的需求,其中大部分是对原有产品的升级,只有ADAS,智能音箱和服务器端的视频识别检测是新的市场。其中智能音箱达到了千万级别,其它的两个还都在扩张。
接下去的服务端的训练硬件,可以用于训练的移动端GPU每个计算核心面积是1.5mm(TSMC16nm),跑在1Ghz的时候能效比是300Gops/W。其他系统级的性能数据我就没有了。虽然这个市场很热,NVidia的股票也因此很贵,但是我了解到全球用于深度学习训练的GPU销售额,一年只有1亿刀不到。想要分一杯羹,可能前景并没有想象的那么好。
最近970发布,果然上了寒武纪。不过2T ops FP16的性能倒是让我吃了一惊,我倒推了下这在16nm上可能是6mm的面积,A73MP4+A53MP4(不含二级缓存)也就是这点大小。麒麟芯片其实非常强调面积成本,而在高端特性上这么舍得花面积,可见海思要在高端机上走出自己的特色之路的决心,值得称道。不过寒武纪既然是个跑指令的通用处理器,那除了深度学习的计算,很多其他场合也能用上,比如ISP后处理,计算结构光深度信息等等,能效可能比DSP还高些。
2009-04-09 22:44 by hyddd, 216765 阅读, 118 评论, 收藏, 编辑
一.CSRF是什么?
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
二.CSRF可以做什么?
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。
三.CSRF漏洞现状
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
四.CSRF的原理
下图简单阐述了CSRF攻击的思想:
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
1.登录受信任网站A,并在本地生成Cookie。
2.在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……)
3.上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
上面大概地讲了一下CSRF攻击的思想,下面我将用几个例子详细说说具体的CSRF攻击,这里我以一个银行转账的操作作为例子(仅仅是例子,真实的银行网站没这么傻:>)
示例1:
银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危险网站B,它里面有一段HTML的代码如下:
首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块……
为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的<img>以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作……
示例2:
为了杜绝上面的问题,银行决定改用POST请求完成转账操作。
银行网站A的WEB表单如下:
后台处理页面Transfer.php如下:
<?php
session_start();
if (isset($_REQUEST[‘toBankId’] && isset($_REQUEST[‘money’]))
{
buy_stocks($_REQUEST[‘toBankId’], $_REQUEST[‘money’]);
}
?>
危险网站B,仍然只是包含那句HTML代码:
和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果…..和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。
示例3:
经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:
<?php
session_start();
if (isset($_POST[‘toBankId’] && isset($_POST[‘money’]))
{
buy_stocks($_POST[‘toBankId’], $_POST[‘money’]);
}
?>
然而,危险网站B与时俱进,它改了一下代码:
<html>
<head>
function steal()
{
iframe = document.frames[“steal”];
iframe.document.Submit(“transfer”);
}
</head>
<body onload=”steal()”>
</body>
</html>
如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块……因为这里危险网站B暗地里发送了POST请求到银行!
总结一下上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个<img>就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。
理解上面的3种攻击模式,其实可以看出,CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!
五.CSRF的防御
我总结了一下看到的资料,CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
1.服务端进行CSRF防御
服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。
(1).Cookie Hashing(所有表单都包含同一个伪随机值):
这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败了:>
在表单里增加Hash值,以认证这确实是用户发送的请求。
<?php
$hash = md5($_COOKIE[‘cookie’]);
?>
<form method=”POST” action=”transfer.php”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<input type=”hidden” name=”hash” value=”<?=$hash;?>”>
<input type=”submit” name=”submit” value=”Submit”>
</form>
然后在服务器端进行Hash值验证
<?php
if(isset($_POST[‘check’])) {
$hash = md5($_COOKIE[‘cookie’]);
if($_POST[‘check’] == $hash) {
doJob();
} else {
//…
}
} else {
//…
}
?>
这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢….由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,这就另外的1%。一般的攻击者看到有需要算Hash值,基本都会放弃了,某些除外,所以如果需要100%的杜绝,这个不是最好的方法。
(2).验证码
这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,厄….这个方案可以完全解决CSRF,但个人觉得在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。
(3).One-Time Tokens(不同的表单包含一个不同的伪随机值)
在实现One-Time Tokens时,需要注意一点:就是“并行会话的兼容”。如果用户在一个站点上同时打开了两个不同的表单,CSRF保护措施不应该影响到他对任何表单的提交。考虑一下如果每次表单被装入时站点生成一个伪随机值来覆盖以前的伪随机值将会发生什么情况:用户只能成功地提交他最后打开的表单,因为所有其他的表单都含有非法的伪随机值。必须小心操作以确保CSRF保护措施不会影响选项卡式的浏览或者利用多个浏览器窗口浏览一个站点。
以下我的实现:
1).先是令牌生成函数(gen_token()):
<?php
function gen_token() {
//这里我是贪方便,实际上单使用Rand()得出的随机数作为令牌,也是不安全的。
//这个可以参考我写的Findbugs笔记中的《Random object created and used only once》
$token = md5(uniqid(rand(), true));
return $token;
}
2).然后是Session令牌生成函数(gen_stoken()):
<?php
function gen_stoken() {
$pToken = “”;
if($_SESSION[STOKEN_NAME] == $pToken){
//没有值,赋新值
$_SESSION[STOKEN_NAME] = gen_token();
}
else{
//继续使用旧的值
}
}
?>
3).WEB表单生成隐藏输入域的函数:
<?php
function gen_input() {
gen_stoken();
echo “<input type=\”hidden\” name=\”” . FTOKEN_NAME . “\”
value=\”” . $_SESSION[STOKEN_NAME] . “\”> “;
}
?>
4).WEB表单结构:
<?php
session_start();
include(”functions.php”);
?>
<form method=”POST” action=”transfer.php”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<? gen_input(); ?>
<input type=”submit” name=”submit” value=”Submit”>
</FORM>
5).服务端核对令牌:
这个很简单,这里就不再啰嗦了。
上面这个其实不完全符合“并行会话的兼容”的规则,大家可以在此基础上修改。
其实还有很多想写,无奈精力有限,暂且打住,日后补充,如果错漏,请指出:>
PS:今天下午写这篇文档的时候FF崩溃了一次,写了一半文章的全没了,郁闷好久T_T…….
转载请说明出处,谢谢[hyddd(http://www.cnblogs.com/hyddd/)%5D
六.参考文献
[1].Preventing CSRF
导读:近期 Uber 宣布将数据库从 Postgres 迁移到 MySQL,在多个技术社区中引起了轩然大波,通过本文我们来详细了解 Uber 做出以上决策背后的原因。
介绍
Uber 的早期架构是由 Python 编写一个单体后端应用程序,使用 Postgres 作为数据持久化。后来 Uber 架构经历一系列显著改变,朝着微服务架构和新的数据平台发展。具体而言,在许多以前使用的 Postgres 的场景,现在更多的使用构建在 MySQL 之上的 schemaless 存储系统(小编:Uber的数据中间件)。在本文中,将探讨一些我们发现的 Postgres 的弊端,并解释我们切换 schemaless 和其他后端服务到 MySQL 数据库的原因。
Postgres 架构概述
我们遇到的大量 Postgres 限制如下:
我们将在所有这些限制,首先通过分析 Postgres 如何组织在磁盘上的表和索引进行分析,特别是比较与 MySQL 使用 InnoDB 存储相同数据的实现方式。需要注意的是,我们在这里提出的分析主要是基于我们有些老的 Postgres 9.2 版本系列的经验。但据我们所知,本文中讨论的 PG 内部架构,并没有显著在新的 Postgres 版本中改变,就如在 9.2 版的磁盘数据设计,也没有比 Postgres 的 8.3 版(10 年前的版本)有什么显著变化。
磁盘数据格式
关系数据库必须执行一些关键任务:
考虑如何将上述这些功能组合在一起工作,是数据库设计时的重要考虑部分。
Postgres 的核心设计之一是不变的(immutable)行数据。这些不变的行在 Postgres 中称为“tuple”。Tuple 在 Postgres 内部实现中由 CTID 来唯一标识 。一个 CTID 代表 tuple 在磁盘上的位置(即物理磁盘偏移)。多个 ctid 可以潜在描述一个单列(例如,当用于 MVCC 目的,或存在的行的多个版本时,行的旧版本尚未被 autovacuum 回收处理)。有组织的 tuple 的集合形成表。表本身具有的索引,通常被组织为 B 树数据结构,映射索引字段到 CTID 的负载。
通常,这些 ctids 对用户透明,但知道他们是如何工作,可以帮助您了解 Postgres 在磁盘上的数据结构。要查看某行当前 CTID,可以在查询的时候显式加上 “CTID”:
为了解释布局的细节,让我们考虑一个简单的用户表的例子。对于每个用户,我们有一个自动递增的用户 ID 的主键,还有用户的名字和姓氏,以及用户的出生年份。我们还定义了用户的全名复合二级索引(姓和名),并在用户的出生年份加上另一个二级索引。创建这样一个表 DDL 可能是这样的:
注意这个定义中的三个索引:主键索引加上两个二级索引。
对于本文中的例子,我们看下表的数据,它由一个选择有影响力的历史数学家开始:
如上所述,每行中隐含有独特的,不透明 CTID。因此,我们可以这样理解表的内部结构:
主键索引,它映射 ID 与 ctids,是这样定义的:
B 树被用在 id 字段上,B 树中的每个节点上保存了 CTID 值。注意,在这种情况下,在 B 树的字段的顺序,刚好与表中顺序相同,这是由于使用自动递增的 id 的缘故,但这并不一定需要是这种情况。
二级索引看起来相似;主要的区别是字段存储顺序不同,因为 B 树,必须按字典顺序组织。姓名索引(first,last)按字母表的顺序排列:
同样,birth_year 聚簇索引按升序排列,就像这样:
正如你所看到的,在这两种情况下,在各自的二级索引 CTID 字段本身并不是有序的,不象第一个自动递增的主键的情况。
假设我们需要更新此表中的记录。举例来说,假设要更新 al-Khwārizmī’ 的出生年份到 770 CE。正如前面提到的,行的 tuple 是不可变的。因此,要更新记录,需要添加一个新的 tuple。这种新的 tuple 有一个新的不透明 CTID,我们称之为 I。Postgres 需要能够从旧的 tuple D 处找到新的 I。在内部,Postgres 存储每个 tuple 中的版本字段,以及指向前一 tuple 的 ctid 指针(如果有)。因此,该表的新结构如下:
只要 al-Khwārizmī 的两个版本存在,索引则必须维护两行的记录。为简单起见,我们省略了主键索引并显示只有在这里的二级索引,它是这样的:
我们将旧版本标识成红色,将新版标识成绿色。在此之下,Postgres 使用另一个字段来保存该行版本,以确定哪一个 tuple 是最新的。这个新增的字段允许数据库确定事务看到的是那一个行的 tuple。
在 Postgres,主索引和二级索引都指向磁盘上的 tuple 偏移。当一个 tuple 的位置变化,各项索引都必须更新。
复制
当我们插入数据到表中,如果启用了流复制机制,Postgres 将会对数据进行复制,处于崩溃恢复的目的,数据库启用了预写日志 (WAL)并使用它来实现两阶段提交(2PC)。即使不启用复制的情况下,数据库也必须保留 WAL ,因为 WAL 提供了 ACID 的原子性(Atomicity)及持久性(Durability)能力。
我们可以通过如下场景来更好的理解 WAL,如果数据库遇到突然断电时意外崩溃,WAL 就提供了磁盘上表与索引更新变化的一个账本。当 Postgres 的守护程序再次启动后,就会对比账本上的记录与磁盘上的实际数据是否一致。如果帐本包含未在磁盘上的体现的数据,则可以利用 WAL 的记录来修正磁盘上的数据。
另外一方面,Postgres 也利用 WAL 将其在主从之间发送来实现流复制功能。每个从库复制数据与上述崩溃恢复的过程类似。流复制与实际崩溃恢复之间的唯一区别是,在恢复数据过程中是否能对外提供数据访问服务。
由于 WAL 实际上是为崩溃恢复目的而设计,它包含在物理磁盘的低级别更新的信息。WAL 记录的内容是在行 tuple 和它们的磁盘偏移量(即一行 ctids) 的实际磁盘上的代表级别。如果暂停一个 Postgres 主库,从库数据完全赶上后,在从库的实际磁盘上的内容完全匹配主库。因此,像工具 rsync 都可以恢复一个同步失败的从库。
Postgres 上述设计的大坑
Postgres 的上述设计给 Uber 在 PG 的使用上,导致了效率低下和其他很多问题。
1. 写放大(Write Amplification)
在 Postgres 设计的第一个问题是已知的写入放大 。
通常的写入放大是指一种问题数据写入,比如在 SSD 盘上,一个小逻辑更新(例如,写几个字节)转换到物理层后,成为一个更大的更昂贵的更新。
同样的问题也出现在 Postgres,在上面的例子,当我们做出的小逻辑更新,比如修改 al-Khwārizmī 的出生年份时,我们不得不执行至少四个物理的更新:
事实上,这四步更新仅为了反映一个到主表的写操作;并且每个这些写入也同样需要在 WAL 得到体现,所以在磁盘上写入的总数目甚至比 4 步更大。
值得一提的是这里更新 2 和 3。当我们更新了 al-Khwārizmī 的出生年份,我们实际上并没有改变他的主键,我们也没有改变他的名字和姓氏。然而,这些索引仍必须与创建在数据库中的行记录了新的行的 tuple 的更新。对于具有大量二级索引的表,这些多余的步骤可能会导致巨大的低效。举例来说,如果我们有一个表上定义了十几个二级索引,更新一个字段,仅由一个单一的索引覆盖必须传播到所有的 12 项索引,以反映新行的 CTID。
2. 复制
因为复制发生在磁盘的变化上,因此写入放大问题自然会转化为复制层的放大。一个小的逻辑记录,如“更改出生年份为 CTID D 到 770”,WAL 会将上述描写的 4 步从网络上同步到从库,因此写入放大问题也等同一个复制放大问题,从而 Postgres 的复制数据流很快变得非常冗长,可能会占用大量的带宽。
在 Postgres 的复制发生一个数据中心内的情况下,复制带宽可能不是一个问题。现代网络设备和交换机可以处理大量的带宽,许多托管服务提供商提供免费或廉价的内部数据中心带宽。然而,当复制必须在不同数据中心之间发生的,问题都可以迅速升级。
例如,Uber 原本使用的物理服务器在西海岸机房。为了灾难恢复的目的,我们在东海岸托管空间添加了一批服务器。在本设计中,我们西部数据中心作为主库,东海岸增加了一批服务器作为从库。
级联复制可以降低跨数据中心的带宽要求,只需要主库和一个从库之间同步一份数据所需的带宽和流量,即便在第二个数据中心配置了多个从库。然而,Postgres 的复制协议的详细程度,对于使用了大量二级索引的数据库,仍可能会导致数据的海量传输。采购跨国的带宽是昂贵的,即使有钱的土豪公司,也无法做到跨国的带宽和本地的带宽一样大。
这种带宽的问题也导致我们曾经在 WAL 归档方面出现过问题。除了发送所有从西海岸到东海岸的 WAL 更新,我们将所有的 WAL 记录归档到一个文件存储的 Web 云服务,这样当出现数据灾难情况时,可以从备份的 WAL 文件恢复。但是流量峰值时段,我们与存储网络服务的带宽根本无法跟上 WAL 写入的速度。
3. 数据损坏
在一次例行主数据库扩容的变更中,我们遇到了一个 Postgres 9.2 的 bug。从库的切换时间顺序执行不当,导致他们中的一些节点误传了一些 WAL 记录。因为这个 bug,应该被标记为无效的部分记录未标记成无效。
以下查询说明了这个 bug 如何影响我们的用户表:
SELECT * FROM users WHERE ID = 4;
此查询将返回两条记录:修改出生年份之前的老记录,再加上修改后的新记录。如果将 CTID 添加到 WHERE 列表中,我们将看到返回记录中存在不同的 CTID 记录,正如大家所预料的,返回了两个不同行的 tuple。
这个问题是有几个原因非常伤脑筋。首先,我们不能轻易找出这个问题影响的行数。从数据库返回的结果重复,导致应用程序逻辑在很多情况下会失败。我们最终使用防守编程语句来检测已知有这个问题表的情况。因为 bug 影响所有服务器,损坏的行在不同的服务器节点上可能是不同的,也就是说,在一个从库行 X 可能是坏的,Y 是好的,但对另一个从库,用行 X 可能是好的,Y 行可能是坏。事实上,我们并不确定数据损坏的从库节点数量,以及主库是否也存在数据损坏。
虽然我们知道,问题只是出现在每个数据库的少量几行,但我们还是非常担心,因为 Postgres 复制机制发生在物理层,任何小的错误格式有可能会导致彻底损坏我们的数据库索引。B 树的一个重要方面是,它们必须定期重新平衡 ,并且这些重新平衡操作可以完全改变树的结构作为子树被移到新的磁盘上的位置。如果错误数据被移动,这可能会导致树的大部分地区变得完全无效。
最后,我们追踪到了实际的 bug,并用它来确定新的 master 不存在任何损坏行。然后再把 master 的快照同步到所有从库上去,这是一个艰苦的体力活的过程(小编:看到美帝的 DBA 也这么苦逼心理终于平衡一点了),因为我们每次只能从在线的池子里面拿出有限几台来操作。
虽然我们遇到的这个 bug 仅影响 Postgres 9.2 的某些版本,而且目前已经修复了很久。但是,我们仍然发现这类令人担忧的 bug 可以再次发生。可能任意一个新的 Postgres 版本,它会带着这种致命类型的 bug,而且由于其复制的不合理的设计,这个问题一旦出现,就会立即蔓延到集群中所有复制链的数据库上。
4. 从库无 MVCC
Postgres 没有真正的从库 MVCC 支持。在从库任何时刻应用 WAL 更新,都会导致他们与主库物理结构完全一致。这样的设计也给 Uber 带来了一个问题。
为了支持 MVCC,Postgres 需要保留行的旧版本。如果流复制的从库正在执行一个事务,所有的更新操作将会在事务期间被阻塞。在这种情况下,Postgres 将会暂停 WAL 的线程,直到该事务结束。但如果该事务需要消耗相当长的时间,将会产生潜在的问题,Postgres 在这种情况下设定了超时:如果一个事务阻塞了 WAL 进程一段时间,Postgres 将会 kill 这个事务。
这样的设计意味着从库会定期的滞后于主库,而且也很容易写出代码,导致事务被 kill。这个问题可能不会很明显被发现。例如,假设一个开发人员有一个收据通过电子邮件发送给用户一些代码。这取决于它是如何写的,代码可能隐含有一个的保持打开,直到邮件发送完毕后,再关闭的一个数据库事务。虽然它总是不好的形式,让你的代码举行公开的数据库事务,同时执行无关的阻塞 I / O,但现实情况是,大多数工程师都不是数据库专家,可能并不总是理解这个问题,特别是使用掩盖了低级别的细节的 ORM 的事务。(小编:美帝程序员代码习惯跟咱们也很类似)
Postgres 的升级
因为复制记录在物理层面工作,这导致不能在不同的 Postgres GA 版本之间进行复制。运行的 Postgres 9.3 主数据库无法复制数据到 Postgres 9.2 的从库上,也无法在运行 9.2 的主数据库复制数据到 Postgres 9.3 的从库上。
我们按照以下这些步骤,从一个 Postgres 的 GA 版本升级到另一个:
我们使用上述方法将 Postgres 9.1 成功升级到 Postgres 9.2。然而,这个过程花了太多时间,我们不能接受这个过程再来一次。到 Postgres 9.3 出来时,Uber 的增长导致我们的数据大幅增长,所以升级时间将会更加漫长。出于这个原因,我们的 Postgres 的实例一直运行 Postgres 9.2 到今天,尽管当前的 Postgres GA 版本是 9.5。
如果你正在运行 Postgres 9.4 或更高版本,你可以使用类似 pglogical,它实现了 Postgres 的一个逻辑复制层。使用 pglogical,可以在不同的 Postgres 版本之间复制数据,这意味着升级比如从 9.4 到 9.5,不会产生显著的停机时间。但这个工具的能力依然存疑,因为它没有集成到 Postgres 主干,另外对于老版本的用户,pglogical 仍然不能支持。
MySQL 架构概述
为了更进一步解释的 Postgres 的局限性,我们了解为什么 MySQL 是 Uber 新存储工程 Schemaless 的底层存储 。在许多情况下,我们发现 MySQL 更有利于我们的使用场景。为了了解这些差异,我们考察了 MySQL 的架构,并与 Postgres 进行对比。我们特别分析 MySQL 和 InnoDB 存储引擎如何一同工作。Innodb 不仅在 Uber 大量使用,它也是世界上使用最广泛的 MySQL 存储引擎。
InnoDB 的磁盘数据结构
与 Postgres 一样,InnoDB 支持如 MVCC 和可变数据这样的高级特性。详细讨论 InnoDB 的磁盘数据格式超出了本文的范围;在这里,我们将重点放在从 Postgres 的主要区别上。
最重要的架构区别在于 Postgres 的索引记录直接映射到磁盘上的位置时,InnoDB 保持二级结构。而不是拿着一个指向磁盘上的行位置(如 CTID 在 Postgres),InnoDB 的第二个索引记录持有一个指向主键值。因此,在 MySQL 中的二级索引与相关联的主键索引键,是如下所示:
为了执行上的(first, last)索引查找,我们实际上需要做两查找。第一次查找表,找到记录的主键。一旦找到主键,则根据主键找到记录在磁盘上的位置。
这种设计意味着 InnoDB 对 Postgres 在做非主键查找时有小小的劣势,因为 MySQL 要做两次索引查找,但是 Postgres 只用做一次。然后因为数据是标准化的,行更新的时候只需要更新相应的索引记录。
而且 InnoDB 通常在相同的行更新数据,如果旧事务因为 MVCC 的 MySQL 从库而需要引用一行,老数据将进入一个特殊的区域,称为回滚段。
如果我们更新 al-Khwārizmī 的出生年份,我们看会发生什么。如果有足够的空间,数据库会直接更新 ID 为 4 的行(更新出生年份不需要额外的空间,因为年份是定长的 int)。出生年份这一列上的索引同时也会被更新。这一行的老版本被复制到回滚段。主键索引不需要更新,同样姓名索引也不需要更新。如果在这个表上有大量索引,数据库需要更新包含了 birth_year 的索引。因此,我们并不需要更新 signup_date,last_login_time 这些索引,而 Postgres 则必须全更新一遍。
这样的设计也使得 vocuum 和压缩效率更高。所有需要 vocuum 的数据都在回滚段内。相比之下,Postgres 的自动清理过程中必须做全表扫描,以确定删除的行。
MySQL 使用额外的间接层:二级索引记录指向主索引记录,而主索引本身包含在磁盘上的排的位置。如果一个行偏移的变化,只有主索引需要更新。
复制
MySQL 支持多个不同的复制模式:
这些模式都各有利弊。基于语句的复制通常最为紧凑,但可能需要从库来支持昂贵的语句来更新少量数据。在另一方面,基于行的复制,如同 Postgres 的 WAL 复制,是更详细,但会导致对从库数据更可控,并且更新从库数据更高效。
在 MySQL 中,只有主索引有一个指向行的磁盘上的指针。这个对于复制来说很重要。MySQL 的复制流只需要包含有关逻辑更新行的信息。复制更新如“更改行的时间戳 x 从 T_ 1 至 T_ 2 ”,从库自动根据需要更新相关的索引。
相比之下,Postgres 的复制流包含物理变化,如“在磁盘偏移8382491,写字节XYZ。” 在 Postgres 里,每一次磁盘物理改变都需要被记录到 WAL 里。很小的逻辑变化(如更新时间戳)会引起许多磁盘上的改变:Postgres 必须插入新的 tuple,并更新所有索引指向新的 tuple。因此许多变化将被写入 WAL。这种设计的差异意味着 MySQL 复制二进制日志是显著比 PostgreSQL 的 WAL 流更紧凑。
复制如何工作也会影响从库的 MVCC。由于 MySQL 的复制流使用逻辑的更新,从库可以有真正的 MVCC 语义; 因此,读库查询不会阻塞复制流。相比之下,Postgres 的 WAL 流包含物理磁盘上的变化,使得 Postgres 的从库无法应用复制更新从而与查询相冲突,所以 PG 复制不能实现 MVCC。
MySQL 的复制架构意味着,bug 也许会导致表损坏,但不太可能导致灾难性的失败。复制发生在逻辑层,所以像一个重新平衡 B tree 这样的操作不会导致索引损坏。一个典型的 MySQL 复制问题是一个语句被跳过(或较少一点的情况,重复执行)的情况下。这可能会导致数据丢失或无效,但不会导致数据库出现灾难问题。
最后,MySQL 的复制架构使得它可以在 MySQL 不同版本之间进行复制。MySQL 只在复制格式改变的时候才增加版本号,这对 MySQL 来说很不常见。MySQL 的逻辑复制格式也意味着,在磁盘上的变化在存储引擎层不影响复制格式。做一个 MySQL 升级的典型方法是在一个时间来更新应用到一个从库,一旦你更新所有从库,你可以把它提为新的 master。这个操作几乎是 0 宕机的,这样也能保证 MySQL 能及时得到更新。
其他 MySQL 设计优势
到目前为止,我们集中于 Postgres 和 MySQL 在磁盘上的架构。MySQL 的架构导致性能比 Postgres 有显著优势。
缓冲池设计
首先,两个数据库缓冲池的工作方式不同。Postgres 用作缓存的内存比起内存的机器上的内存总数小很多。为了提高性能,Postgres 允许内核通过自动缓存最近访问的磁盘数据的页面缓存。举例来说,我们最大的 Postgres 的节点有 768G 可用内存,但只有大约 25G 的内存实际上是被 Postgres 的 RSS 内存使用,这让 700 多 GB 的可用内存留给 Linux 的页面缓存。
这种设计的问题是,相比访问 RSS 内存,操作系统的页面缓存访问数据实际上开销更大。从磁盘查找数据,Postgres 执行 lseek 和 read 系统调用来定位数据。这些系统调用的招致上下文切换,这比从主存储器访问数据更昂贵。事实上,Postgres 在这方面完全没有优化:Postgres 没有利用的 pread(2)系统调用,pread 会合并 seed + read 操作成一个单一的系统调用。
相比之下,InnoDB 存储引擎实现了自己的 LRUs 算法,它叫做 InnoDB 的缓冲池。这在逻辑上类似于 Linux 的页面缓存,但在用户空间实现的,因此也显著比 Postgres 设计复杂,InnoDB 缓冲池的设计有一些巨大的优势:
连接处理
MySQL 的实现是对每个连接生成一个线程,相对来说开销较低;每个线程拥有堆栈空间的一些内存开销,再加上堆上分配用于连接特定的缓冲区一些内存。对 MySQL 来说扩展到 10,000 左右的并发连接不是罕见的事情,实事上我们现在的 MySQL 接近这个连接数。
Postgres 使用的是每连接一个进程的设计。这很明显会比每连接每线程的设计开销更大。启动一个新的进程比一个新的线程会占用更多的内存。此外,线程之间进行通讯比进程之间 IPC 开销低很多。Postgres 9.2 使用系统V IPC为IPC原语,而不是使用线程模型中轻量级的 futexes,futex 的非竞争是常见的情况,比 System V IPC 速度更快,不需要进行上下文切换。
除了与 Postgres 的设计相关联的内存和 IPC 开销,即使有足够的可用内存可用,Postgres 对处理大连接数的支持依然非常差。我们已经碰到扩展 Postgres 几百个活动连接就碰到显著的问题的情况,在官方文档中也没有确切的说明原因,它强烈建议使用独立的连接池来保证大连接数。因此,使用 pgbouncer 做连接池基本可行。但是,在我们后端系统使用过程中发现有些 BUG,这会导致开启大量的原本不需要的活跃连接,这些 BUG 也已经造成好几次宕机。
结论
Postgres 在 Uber 初期运行的很好,但是 PG 很遗憾没能很好适应我们的数据增长。今天,我们有一些遗留的 Postgres 实例,但我们的数据库大部分已经迁移到 MySQL(通常使用我们的 Schemaless 中间层),在一些特殊的情况下,也使用 NoSQL 数据库如 Cassandra。我们对 MySQL 的使用非常满意,后续可能会在更多的博客文章中介绍其在 Uber 一些更先进的用途。
作者 Evan Klitzke 是 Uber 核心基础架构组资深软件工程师。他也是一个数据库爱好者,是 2012 年 9 月加入 Uber 的一名早鸟。
英文原文:
最近很多朋友都在问我关于 Flask 部署的问题,说实在的我很乐意看到和回答这样的问题,至少证明了越来越多人开始用 Flask 了。
之前我曾发表过一篇在 Ubuntu 上用 uwsgi + nginx 的 Flask 部署方法,说实在的 uwsgi 是个大坑可能用在 Django 上还好吧,不过用在 Flask 上未必就如此。至少 , uwsgi 是个极为折腾人的东西。总之,我是一直认为复杂的东西未必不好,但一定是不好用的。