Server中为什么需要分配new分配的内存空间间给回调函数

是指在显示器上为了显示出图像而经过的一系列必要操作。
渲染管道中的很多步骤,都要将几何物体从一个坐标系中变换到另一个坐标系中去。

mono是.net的一个开源跨平台工具,就类似java虚拟机,java本身不是跨平台语言,但运行在虚拟机上就能够实现了跨平台。.net只能在windows下运行,mono可以实现跨平台跑,可以运行于linux,Unix,Mac OS等。

三十二:简述Unity3D支持的作为脚本的语言的名称

Unity的脚本语言基于Mono的.Net平台上运行,可以使用.NET库,这也为XML、数据库、正则表达式等问题提供了很好的解决方案。Unity里的脚本都会经过编译,他们的运行速度也很快。这三种语言实际上的功能和运行速度是一样的,区别主要体现在语言特性上。JavaScript、 C#、Boo

三十三:U3D中用于记录节点空间几何信息的组件名称,及其父类名称

三十四:向量的点乘、叉乘以及归一化的意义?

Framework CLR 的,在可移植性,可维护性和强壮性都比C++ 有很大的改进。C# 的设计目标是用来开发快速稳定可扩展的应用程序,当然也可以通过Interop 和Pinvoke 完成一些底层操作。更详细的区别大家可以

四十:结构体和类有何区别?

结构体是一种值类型,而类是引用类型。(值类型、引用类型是根据数据存储的角度来分的)就是值类型用于存储数据的值,引用类型用于存储对实际数据的引用。那么结构体就是当成值来使用的,类则通过引用来对实际数据操作

四十一:ref参数和out参数是什么?有什么区别?

ref和out参数的效果一样,都是通过关键字找到定义在主函数里面的变量的内存地址,并通过方法体内的语法改变它的大小。不同点就是输出参数必须对参数进行初始化。ref必须初始化,out 参数必须在函数里赋值。ref参数是引用,out参数为输出参数。

四十二:C#的委托是什么?有何用处?

委托类似于一种安全的指针引用,在使用它时是当做类来看待而不是一个方法,相当于对一组方法的列表的引用。用处:使用委托使程序员可以将方法引用封装在委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象,而且是类型安全的。

四十三:C#中的排序方式有哪些?

选择排序,冒泡排序,快速排序,插入排序,希尔排序,归并排序

四十四:射线检测碰撞物的原理是?

射线是3D世界中一个点向一个方向发射的一条无终点的线,在发射轨迹中与其他物体发生碰撞时,它将停止发射 。

四十五:Unity中,照相机的Clipping Planes的作用是什么?调整Near、Fare两个值时,应该注意什么?

剪裁平面 。从相机到开始渲染和停止渲染之间的距离。

四十六:如何让已经存在的GameObject在LoadLevel后不被卸载掉?

13.下列关于光照贴图,说法错误的是?(C)

A.使用光照贴图比使用实时光源渲染要快

B.可以降低游戏内存消耗

C.可以增加场景真实感

D.多个物体可以使用同一张光照贴图

14.如何为物体添加光照贴图所使用的UV?(B)

A.不用添加,任何时候都会自动生成

C.更改物体导入设置,勾选“Swap UVs”

17.关于Vector3的API,以下说法正确的是?(C)

18.下列那些选项不是网格层属性的固有选项?(B)

19.写出你对游戏的理解及游戏在生活中的作用,对Unity3D软件理解最深入的地方

  引言 GDB7.0 是 2009 年 10 月份正式发布的。和多数程序员一样,那则消息并不曾引起我的注意,因为 gdb 为数不多的几个新版本都让人觉得非常平淡。没有让人振奋的新特性。
  一晃几个月过去了,随意浏览 gdb 主页的时候,我突然发现一个叫做反向调试 (reverse debug) 的特性,默默地列在不引人注目的地方。"反向调试"?我们调试总是下一步,下一步,反向调试就是上一步,上一步了?
  经过简单的试用,我发现这是一个非常有用的特性,值得我们去学习和掌握。利用该功能,您可以让被调试进程反向执行。您可能会问,这有什么用处呢?
  嗯,我觉得软件调试往往是一个猜测的过程,一般的普通人似乎不太可能第一次就将断点设置在最正确的位置,所以我们经常会发现重要的代码执行路径已经错过。比如运行到断点之前,程序的某些状态就已经不正确了。以往,我们只好退出当前调试会话,从头再来。
  每次错误的猜测都必须让一切从头再来,如果您的运气不佳,很快就会觉得非常抓狂吧。
  假如有反向调试功能,在这种情况下,我们无须重新启动调试程序,只要简单地让被调试进程反向执行到我们怀疑的地方,如果这里没有问题,我们还可以继续正向执行调试,如此这般。如同我们在学习英语时使用复读机一样,来回将听不懂的部分重放,分析。这无疑将极大地提高工作效率。 反向调试的使用简介
  反向调试的基本原理为 record/replay。将被调试进程的执行过程录制下来,然后便可以像 DVD 一样的任意反向或正向回放。因此,为了使用反向调试,您首先需要使用 record 命令进行录制。
  此后的调试过程和您以前所熟悉的过程一样,不过您现在多了几个反向控制的命令: 让我们假设您已经使用"break 10"命令在程序的第 10 行设置断点。然后输入"run"让进程开始执行。不久,程序会暂停在第 10 行代码处,控制权返回 gdb,并等待您的命令。此时如果您使用 next 命令,那么程序会顺序执行第 11 行代码,如果您使用 reverse-next,那么程序将回退执行第   使用反向调试命令之后,任何时候,您还可以自由地使用其他的 gdb 命令。比如 print 来打印变量的值等等。非常方便。 反向调试的实现原理
  除了使用这项特性所带来的好处之外,或许更令人着迷的是该特性的实现原理吧。我们都不曾见过时光倒流,能够回头执行指令的处理器也貌似从未出现过,那么 gdb 是如何实现反向执行的呢?
  为了说明这个问题,首先我们需要回顾一些 gdb 的基本概念。
  GDB 的基本概念
  Gdb 的一些重要术语以及 gdb 的整体结构
  进入一个陌生的国家前先学几句他们的常用语会比较好。GDB 这个小世界中也经常使用一些特有的名词,我们最好首先熟悉他们。
  Exec,指一个可执行文件,可以是 ELF 格式,也可以是古老的 a.out。
  Inferior,指一个正在运行的 exec,一般就是指被调试进程。
  接下来最好我们能够对 gdb 有一个整体的,高度概括的了解。
  Gdb 的设计目标是为各种平台上的人们提供一个通用的调试器,因此它必须有可扩展性,以便人们可以将它移植到不同的硬件和软件环境下。
  为了实现这个目标,GDB 的体系结构采用分层和封装的设计思想,将那些依赖于特定软硬件环境的部分进行抽象和封装。最重要的两个封装概念便是 gdbarch 和 target。他们和 gdb core 之间的关系大致可以用下图来描述:
  当需要在不同的 OS 上运行 GDB 时,只需提供相应的 target 便可;同样,当需要支持一种新的新的处理器时,也只需提供新的 gdbarch,而 gdb core 则无需任何修改。
  Gdbarch 是一个封装了所有关于处理器硬件细节的数据结构。在这个数据结构中,不仅包括一些描述处理器特性的变量,也包括一些函数(喜欢 OO 的人会自然地联想到类。)这些函数实现了对具体硬件的一些重要操作,比如分析 stack frame,等等。
  完整的 gdbarch 数据结构非常庞大,无法一一列出,下表分类总结了 gdbarch 中的重要信息: 描述硬件体系结构和 ABI 细节的信息 比如 :
  比如 return_value:描述该处理器 ABI 中规定的处理函数返回值的方法
  访问和分析 stack frame 的函数 不同体系结构的 stack frame 都不尽相同。这些函数提供了如何分析和创建 stack frame 的具体实现函数。
  可以看到 gdbarch 封装了所有 gdb 运行时所需要的硬件信息,以及如何访问和处理这些信息的具体函数。类似于面向对象设计中的类的设计,将关于处理器硬件细节的数据和方法都封装到 gdbarch 数据结构中。
  同 gdbarch 一样,target 也是一种封装。但 target 所封装的概念更复杂一些。它不仅封装某一种操作系统,也封装了不同的"调试方式"。
  首先,不同的操作系统对应不同的 target。同样在 i386 处理器下工作,Linux 和 vxworks 对于 debug 的支持是不同的,比如如何创建进程,如何控制进程等。这些不同对于 gdb core 是透明的,由 target 来屏蔽。
  此外,target 还封装了不同的"调试方式"。这个词比较抽象,最好是举例说明。
  比如,同样是在 i386 Linux 下面,您即可以使用 native 方式调试 exec,也可以调试一个 core dump 文件,还可以 attach 一个正在运行的进程并调试它。打开一个可执行文件和一个 core dump 文件的方法是不同的,同样,将一个可执行文件 load 进内存执行和 attach 到一个正在执行的进程也是不同的。
  对于这些不同,gdb 也采用 target 进行封装。对于 gdb core 来说,当它需要让一个调试目标开始运行时,便调用 target 相应的回调函数,而不必关心这个回调函数如何实现。启动进程的具体的细节由不同的 target 来具体实现。当 target 为 exec,即一个磁盘上的可执行文件时,可以使用 fork+exec 的方式;当 target 是一个远程调试目标时,可以通过 TCP/IP 发送一个命令给 gdb server 进行远程调试;当 target 是一个已经在运行的进程时,需要使用 ptrace 的 attach 命令挂载上去。诸如这些细节,gdb 统统由 target 这个概念来封装。
  这便是 target 这个概念的主要意义,不过,还有一些事实让 target 更加复杂。
  有时候,人们希望在同一个 gdb 会话中调试多个 target。最常见的例子是调试 core dump 文件时,往往需要同时打开产生 core dume 的可执行文件,以便读取符号。
  此时人们往往还需要用 file 命令打开 a.out 程序,即一个 exec。
  除了 core dump 分析,还有其它一些情况要求 gdb 同时管理多个 target。为了应对这些需求,gdb 对 target 采用了分层、优先级管理的堆栈模式。堆栈中的每一层由一个形如 xyz_stratum 的古怪名字来标示,如下图所示: 这个堆栈的优先级从上到下递增,gdb 总是采用最高优先级 target 所提供的函数进行操作。
inferior。您在稍后的章节中可以看出,这种分层结构对反向调试的实现非常有帮助。
  下表列出了 target 数据结构的重要内容: 控制调试目标的函数 比如 :
  GDB 运行时的基本流程
  对一个目标进程进行调试,需要操作系统提供相应的调试功能。比如将一个正在运行的进程暂停并读写其地址空间等。在传统 Unix 中,一般由 ptrace 系统调用提供这些功能。本文不打算详细介绍 ptrace,读者可以通过参考资料 [5] 获得更详细的介绍。
  但 ptrace 的编程模式极大地影响了 gdb 的设计,下面我们研究 gdb 如何使用 Ptrace。
  首先,gdb 调用 ptrace 将一个目标进程暂停,然后,gdb 可以通过 ptrace 读写目标进程的地址空间,还可以通过 ptrace 让目标进程进入单步执行状态。Ptrace 函数返回之后,gdb 便开始等待目标进程发来的信号,以便进一步的工作。
  以单步执行为例,gdb 调用 ptrace 将目标进程设置为单步执行模式之后,便开始等待 inferior 的消息。因为处于单步模式,inferior 每执行一条指令,Linux 便将该进程挂起,并发送一个信号给 ptrace 的调用者,即 gdb。Gdb 接受到这条信号 ( 通过 wait 系统调用 ) 后,便知道目标进程已经完成了一次单步,然后进行相应处理,比如判断这里是否有断点,或进入交互界面等待用户的命令等等。
  这非常类似窗口系统中的消息循环模式。Ptrace 的这一工作模式影响了整个 gdb 的设计,无论具体的 target 是否支持 ptrace,gdb 都采用这种消息循环模式。
  理解了以上的基础知识,您就可以开始探索反向调试的具体实现细节了。
  反向调试原理和代码导读
  如前所述,反向调试的基本原理是录制回放。它将 inferior 运行过程中的每条指令的执行细节录制下来,存放到 log 中。当需要回退时,从 log 中读取前一条指令执行的细节,根据这些细节,执行 undo 操作,从而将 inferior 恢复到当时的状态,如此便实现了"上一步"。
  undo 就是将某条指令所做的工作取消。比如指令 A 将寄存器 reg1 的值加了 1,那么 undo 就是将其减一。
  原理很简单,然而要将此想法付诸实现,人们必须解决几个具体的问题:
  如何录制,又如何回放呢?
  其次,当我们说到一条指令的执行细节时,究竟是指那些具体内容呢?或者说我们究竟应该录制些什么呢?这些记录如何组织?这便是 record list 的主要内容。下面我们一一来了解这些知识。
  反向调试引入了一个新的 target,叫做"record",它工作在 target stack 的第二层。Gdb target 的分层结构带来了这样一种好处:高层的 target 可以复用底层 target 的功能,并在其上添加额外的功能。我想我们可以这么说:低层 target 完成基本的低级功能,高层 target 完成更高级的功能。
  Record target 就是一个带录制功能的高层 target,它依赖低层 target 完成诸如启动进程,插入断点,控制单步等基本功能。在此之上,它将 inferior 执行过程中的每条指令的细节记录下来。此外,它还处理几个反向调试特有的命令,reverse next 等。
  当用户希望进行反向执行时,record target 并不需要低层 target 的帮助。因为 inferior 的执行过程都已经被记录在一个 log 中,反向执行时,record target 从 log 中读取记录,并 undo 这些记录的影响,比如恢复先前寄存器的值,恢复被指令修改的内存值等,从而达到了反向执行的效果。
  下面我们详细分析几个重要的 record target 所提供的函数,从而对上述基本思想有更深入的理解。
中)完成基本的 resume 工作。 这里需要注意一点,在调用 record_beneath_to_resume时,第三个参数 step为 1,即单步执行。这是因为 record target需要录制目标进程的每条指令,因此假如用户命令为 continue或 next,而不是 step时 ,目标进程将继续执行下去直到遇到断点为止,在此期间的指令 gdb无法获知,便也无从记录。因此   该函数首先判断是否需要进行录制,如果需要,则进一步判断当前的 inferior 是否是单步执行状态,如果是,则不需要进行录制,因为马上 inferior 就会停下来,而 gdb 再次让 inferior 恢复执行时将调用 record_resume,那里会执行录制工作。
  但如果当前的 inferior 不在单步状态,且下一条指令不是断点,那么如果让 inferior 继续执行则意味着 record target 将错过后续的指令执行而无法进行录制。因此,在这种情况下,record_wait 将进入一个循环,在每次循环迭代中执行录制,并让 inferior 进入单步执行状态,直到遇到断点或者 Inferior 执行 exit 为止。伪代码如下: 这样,通过 record_wait 的处理,inferior 的每条执行指令都将被录制下来。
  Record_wait 的另外一半代码是处理 replay 的。假如当前用户希望反向执行,那么 record_wait 就从日志中读取 inferior 上一条执行指令的相关记录,恢复寄存器,内存等上下文,从而实现"上一步"的操作。
  每次执行一条指令,record target 便将关于该指令的信息录制下来。这些信息可以完整地描述一条指令执行的效果。在目前的 record target 中,记录的信息只包括两类:寄存器和内存。
  一条机器指令能够改变的就是寄存器或内存。因此每次执行一条指令,record target 对该指令进行分析,如果它修改了内存,那么便记录下被修改的内存的地址和值;如果它修改了寄存器,便记录下寄存器的序号和具体的值。
  函数 do_record_message 具体完成指令执行的录制细节。抛开 gdb 代码的层层调用细节,该函数的具体工作是调用 gdbarch 所提供的 process_record 回调函数。
  对于 i386,具体的 process_record 函数为 这是一个 1000 多行的巨型函数,我建议大家不必精读其中的每一行代码。。。
  大体说来,该函数首先反汇编正在执行的机器指令,根据反汇编的结果分析该指令是否修改了寄存器或者内存。如果有所修改,就分别分配新的 reg entry 或者 mem entry 并插入到 record_list,当对该指令的所有执行结果都分配了相应的 record_entry 之后,调用 record_arch_list_add_end 插入一个 end   Record target 的录制过程用时序图来表示比较容易理解,因为将相关操作串起来的是事件而不是函数调用关系。假如您打算跟踪函数的调用关系,那么很快就会迷失到晕头转向。参考资料 [7] 是我看到的最好的关于 gdb 的文档,我觉得其中最棒的部分就是 gdb 命令执行时的时序图,这是一种非常好的表示方法。下面我打算用时序图来完整地描述前面罗罗嗦嗦几千字却依然描述不清的东西。
  最简单的 record 命令序列为:
  Record_wait 将完成剩余的录制过程。因为当前的用户命令为 continue,因此 record_wait 将进入代码清单 2 所示的循环,循环执行下列操作: 录制当前指令
  调用下一层的 resume 函数并将 step 参数设置为一,强制 inferior 进入单步执行。
  由于单步执行,inferior 在执行完一条指令后又将 gdb 的 wait 操作唤醒,继续上述循环。如此循环往复,直到遇到断点或者执行到 exit 为止,从而完成录制过程。
  Recored target 回放功能的实现比较简单,本文长度有限,读者可以自行分析。 GDB 反向调试的局限
  Gdb 的反向调试是从 2006 年左右开始研发的,虽然目前已经正式发布。但还是不太稳定,且有一些局限。简述如下:
  有 side effect 的语句虽然能够回退执行,但其所造成的 side effect 则无法撤销。比如打印到屏幕上的字符并不会因为打印语句的回退而自动消失。
  因此,反向调试不适用于 IO 操作。
  此外 , 支持反向调试的处理器体系结构还很有限 , 需要更多的研发人员参与进来 . 结束语
  很多人都在问,反向调试究竟有多大的实际用处?我在本文的开头便简单介绍了一种使用它的场景,但我想这并不能令心存怀疑的人满意。实际上,以我的个人经验来看,50% 的程序员从来不使用调试器。对于很多实际工作,即使不使用调试器,通过不断的打印和代码分析最终也能够解决问题。但假如能正确地使用调试器,或许能够更加有效地解决问题,从而将人生的宝贵时间使用在其他更有意义的地方。正如 Norman

应百林哲笑含的邀请,于号至7.1号前往广州白云国际会议中心参加《CSDI Summit 中国软件研发管理行业技术峰会》。会上认识了很多互联网一线老师是最大的收获:

本次我分享的主题是《兼顾灵活与性能的nginx》:

意外的惊喜是CSDI的讲师证书非常精美:

最后附上本次演讲的PPT内容:

兼顾灵活与性能的nginx

大家好,我是杭州市智链达数据有限公司的联合创始人和CTO,为什么又要介绍一下?因为我们公司是一个互联网服务企业,但是我们面向的客户是建筑企业,所以在座的各位都不是我的潜在客户,所以接下来不会有任何介绍关于我们产品的推广和介绍:-)。从我的这个分享的标题中可以看到,这里其实有两个关键词,一个是性能,一个是灵活,我们接下来讨论这两点中nginx是怎么做到的。当前nginx已经是所有的互联网企业的一个标配底层组件,所以能分享这样一个大众化的广为使用的工具,我个人感到很荣幸。不管是小流量还是大流量场景使用了nginx后都可以有一个立竿见影的效果。

那么本来的话nginx介绍这部分可以没有,但是我相信在座的各位应该在生产环境中时实际操作过nginx的同学应该不是很多吧?能不能请在生产环境中直接使用过nginx,或者你带的团队负责nginx的同学,能不能举一下手?我看一下还是有一半以上的同学没有操作过nginx的,所以我会用五分钟的时间先做个简单的介绍。

首先我们肯定是先看它的使用场景,那么场景的话呢先从这个最右边的这个静态资源来看,我们现在不管是开发一个web页面或者是开发一个APP webview,都会去拉取大量的CSS、JS、小图片等资源,这些资源的空间占用量其实很大,也很难放在内存中,所以只能放在磁盘上。nginx非常擅长把磁盘中的内容以http协议的方式返回给客户端,所以这是nginx第一个场景。第二个场景中,如果我们的应用服务是用python写的,可能也就几百QPS,JAVA写的服务可能有上千QPS,如果是GOLANG写的可能有上万QPS,但是nginx拥有上百万QPS的系统能力,所以如果我们需要尽量提高我们的这个系统容量,那么需要把很多的应用服务组成一个集群来对用户提供服务,在早期的时候,我们可能会用DNS等手段做负载均衡,而现在呢都是采用nginx做反向代理,因为它卓越的单机性能很适合该场景。那么在反向代理使用场景中就会有另外一个问题,负载均衡,我们经常会需要扩容,宕机容灾时这个负载均衡可以发挥很好的作用。那么有了反向代理后又会引出另外一个问题就是缓存。

因为其实在互联网行业中,只要我们想提升用户的体验,基本上都是在缓存上下功夫,而缓存基本上你是放在离用户越近的地方效果越好。比如说你放在手机APP的存储上,或者放在浏览器的storage里,这样的用户体验最好!或者说再差一点到网络中了,那么放在cdn效果也是很好,但如果请求到了我们企业内网中,这个时候,往往离用户最近效果最好,比如说像mysql数据库它虽然专注于只做一件事,以致于他的这个缓存做的是非常厉害,但是他的能力再强也没有用,因为数据库前面会有一个业务应用服务,应用服务强调的是快速迭代,它强调的是对程序员友好以提升开发效率,所以呢你想它性能好是不可能的。所以这个时候呢我们在这个nginx上做缓存,因为反向代理协议就很简单,做缓存也很方便,我们最后也可以拿到好的结果。

第三个场景呢就是中间这个广场。有一些高频的接口调用,比如说像用户鉴权,还有像前天有一位阿里巴巴国际部的老师说他们的流量导流等应用,这些东西都需要这个nginx要发挥自己的特长,然后不要跟慢吞吞的应用服务扯上关系。就像我刚刚说的,其实数据库例如mysql他的能力是很强的,那么如果这些业务可以直接在nginx上实现,那么其实我们就可以提供一个API。那么API服务实现上有几个难点,第一个呢以前的nginx往往是通过每个第三方模块自行定义它自己独特的配置格式,以此实现复杂的业务功能,但这种模式是会有很多问题,因为你是独特的不是通用的,而且且学习成本很高,扩展性也不好。所以呢以通用编程语言实现是一个好思路,官方还搞了一个javascript版本,而openresty搞了一个lua版本,那么因为引入了编程语言,那么你可以很方便的调用工具SDK,所以做API服务就有了可行性,这是最主要的应用场景。

OK那么再看一下nginx的进程架构,我相信在坐的各位都知道nginx的master/worker进程模型。但是我不知道大家有没有想到为什么是一个多进程的架构?可以横向对比一下,比如说nodejs,比如说redis,他们都有一个明显的问题:就是没有办法使用多核。那么nginx呢,因为他在定义核心目标时,他想做的事就是在我们企业内网最外层,获取这台服务器的极限能力以实现上述功能,所以nginx希望能够有效的耗尽cpu、内存等资源。那么master进程它其实非常轻量级,他只是去监控,只是去管理,虽然master也提供了钩子方法供第三方模块介入,但其实像很多第三方模块并不会在master里面做文章,因为只要你有业务在master里就引入了不确定性,master是不能挂,挂了以后你没有办法管理实际工作的worker进程。如果我们做缓存的时候,其实又多了两个进程cache

再看一下编译方式。nginx通常是采用把官方的源代码和这个第三方模块的源码放在一起编译出一个二进制可执行文件,那怎么编译?官方他会提供一个叫bash脚本叫configure,而Tengine或者openresty也会提供自己的configure脚本,它负责把这些源代码以有序的方式整合在一起(下面会说到)。那么编译出来这个二进制文件之后呢我们就可以运行了。近期的版本nginx向大家提供了动态模块的功能,就像windows操作系统中的dll,或者linux操作系统中的so,它们又提供了一层封装,降低了耦合度。就像图中所示,这时候就会把这个动态库打开,把其代码加载至nginx的进程地址空间中,那么有了这个动态模块好处在哪里?如果说我只修改了一个模块,并未对其他模块发生变动,那么就不用重新编译出新的可执行文件了,我只要去换一下这个动态库就可以。

灰度发布可能大家都比较清楚,这是nginx的基本功能了,我们简单过一下。比如说上面这张图,master进程上拉起了四个绿色的worker进程,这四个worker进程用的是老配置文件里的配置,然后呢我们修改了nginx.conf配置文件,调用了-s reload命令后,master进程会起四个黄色的worker进程,这四个黄色的worker进程使用了新的配置文件里的内容。如果我们自己去写一个应用程序实现配置修改时,不知道大家会怎么写?那我可能会想,把应用进程kill掉,再重新拉起读取配置不就完了吗?那是nginx不能这么做,因为它上面真的跑了几十万个tcp连接,所以如果他被kill掉,实际上几十万的客户端都会收到RST复位包,体验是非常差的。所以nginx要采用这么一个复杂的形式,就是绿色的worker进程还在处理老的连接与请求,而黄色的worker进程就只处理新建立的连接与请求,等到绿色的老worker进程处理完老连接上的请求后,我们再停掉worker进程就没有问题了。但是说起来很简单,其实还是有许多问题要处理,比如说如果是HTTP这样的请求那还比较好,即使是keepalive连接也OK,但如果是websocket协议或者其他TCP协议怎么办?nginx不解析协议内容,不知道什么时候可以准确的判断处理完一个请求,这样粗暴的方式就可能直接断用户连接。worker_shutdown_timeout这个配置就是做这个事的。

那我们现在进入第二部分谈谈nginx的模块化。模块化决定了nginx的能力,比如说TCP这个协议,它是上世纪70年代就发明了,我们中间可能有各种各样的改进,比如说拥塞控制等等,但是到了现在还是非常好用。其实nginx也一样,我们掌握了它的模块化思想,就理解了它的底层能力。nginx的模块我不知道如果让你去设计,你会怎么思考?看下这个图,首先它会有核心模块core,这下面的四个模块是框架与工具模块,比如说这个errlog是记录错误日志的。这上面这四个核心模块,每一个都定义了一类新模块。这个就比较关键,那么像这个mail等模块并不重要,我们重点还是看http模块。所有的http模块他们其实就构成了一张数组列表,这个列表里面的第一个元素也是第一个http模块都有个关键字_core_,包括mail、stream模块都是一样的。那么每类模块列表里的第一个core模块存在的意义在哪里呢?它们负责处理本类模块里的共性规则。第一个共性的东西就是协议,比如http协议,一定先是一个请求,请求中先收到url及版本号等,再收到header包头,那发送响应的时候也是先发送line再发送header再发送body。那么我就可以把这个逻辑抽象出来。接下来我们会重点看逻辑是如何抽象出来的。然后呢可能还有一些公共的工具,也可以由第一个core模块来做。

那么http response的响应过滤也有许多共性的抽像,因为我们返回http协议内容的时候可以做很多事情,比如说做压缩、生成缩略图,这个时候呢其实就是在body和header上做文章,所以nginx又引申出来一个概念叫响应过滤模块。我们现在看这张图,先看左边这边传播非常广泛的官网图片,从这上面到达一个internet request,首先读完它的header头部,读完以后呢,这时候判断我的安全模块是否应生效,包括用location正则表达式匹配url决定用哪些配置,我可能会做一些限速。生成response响应内容就是generate content,可能我读本地的磁盘文件实现,也可能我跟上游的一个服务交互获取其内容等等,这个事情做完以后,我拿到了http response,此时开始对header和body进行过滤模块的处理,做完以后呢记录access日志,这个日志用来做监控运维,最后把response返回给用户。

再来看HTTP模块抽象出的11个阶段。比如说现在有一个realip模块,如果需要你去实现这个模块去获取用户的真实IP,我不知道你会把该模块放在哪个阶段?因为实际TCP连接它实际上是个四元组,当nginx与用户中间有反向代理的话,其实你从连接的source ip获取到的地址不是用户的,怎么办呢?除了复杂的proxy protocol等方案外,应用层的HTTP协议很有办法,它可以通过把用户的原始ip在request http header中带来。所以如果我们去实现这样的一个http模块,可以搞一个nginx变量去存取header头部的ip值,这个http模块到底放在11个阶段的哪个阶段就是个问题,因为前面的这些模块都有可能去改http header的,只要其他http模块有能力去改,realip模块获取到的ip可能就有问题。所以,在post_read模块,顾名思义,就是刚读取完http request header,此时去处理最合适。再到下面是rewrite相关的三个阶段,一个叫find config在其中。rewrite模块是官方模块处理的。

接下来看访问控制相关的3个阶段。比如说输入用户名和密码自然在access阶段,但是如果说你要做流控,你肯定要在他之前,否则的话这个流控可能就失效了,请求流量有机会打到access阶段这里。因此流控必须在pre_access阶段。OK现在看一下这个http模块的工作流程,我做了一个动画,怎么看呢?先从左边看这个绿框,这个绿框表示master进程在启动,它首先读取nginx.conf配置文件,这里会依次从上至下读取这个ascii文件,当它发现events {}配置时,自动将大括号里的内容交给event事件模块解析,而发现http{}时就交给http模块处理,stream{}里的内容自然交给stream模块处理。对于http模块而言,有些http模块会根据配置项决定是否将其钩子函数添加至处理流程中。http core模块会负责将所有的location建立为一颗树以加快访问速度,最后还要将listen后跟着的端口加入容器中,接着读完配置文件就开始listen打开监听端口,再fork出子进程。子进程worker会继承父进程已经打开的句柄,自然也包含端口。master进程接下来就只监控worker子进程,以及等待接收信号命令好了。而worker进程则只处理事件以及接收master发来的信号命令。当worker进程收到SYN包,开始建立连接,请求处理流程就开始了。首先用HTTP状态机确保接收到完整的HTTP的header头部,再依次调用刚刚介绍过的11个阶段的HTTP模块去处理请求,在content阶段处理完后生成了http

再看看怎么找到location下的配置去处理请求的。tcp连接是四元组,所以可以根据lister时的ip address去获取连接,寻找由哪些server{}去建立连接,就像图中的最左边,其配置如下所示:

接着,我们接收完http request header后,可以从HOST头部获取到域名,而这可以匹配server_name配置后的虚拟主机域名列表,这样就唯一确定了一个server{}配置块。从URL中还可以再次匹配location后的正则表达式,这样我们就找到了具体的location配置。

再看这张图,我们谈谈11个阶段间http模块间的配合。这里仅以官方模块举例。当一个请求读完http header后,我们先进入preaccess阶段,这一阶段里有两个模块:limit_conn和limit_req模块。前前限连接,后者限请求。可见,前后顺序乱不得,否则就导致limit_conn无法正常生效了。当limit_conn模块决定请求不受限制后,它会返回NGX_OK给钩子函数,这样进入当前preaccess阶段的下一个模块limit_req模块继续处理。而limit_req模块也认为不受限制,可以继续处理,因为当前preaccess阶段没有其他模块了,故进入下一阶段access阶段继续处理。而在access阶段中,若第一个模块auth_basic认为无须进入下一个access模块处理,那么它可以返回NGX_AGAIN给钩子函数的调用者,这样access阶段其后的模块是得不到执行的。可见http模块还是很灵活的。当content阶段生成内容后,首先由header filter模块处理。为什么呢?因为http是流式协议,先返回header,再返回body。比如我需要做压缩,那么就需要先在header中添加content-encoding头部,再压缩body。这里需要注意的是,这些模块间也有顺序要求!比如现有一张图片,你只能先做缩略做再压缩,如果反过来,压缩后是没办法做缩略图的。所以这个顺序也是由configure这个脚本决定的,大家可以看源码时看到里面的注释明确的写着不能改order顺序。

这张图是openresty官方的图。用好openresty的关键是,搞清楚指令与sdk。其中sdk比较简单,就是形如ngx.xxx这样的函数,可以在lua代码中调用。它实际上就是lua与C语言的交互,通过先在nginx模块中提供相应的函数,再封装给lua作为lua函数即可,目前主要在用ffi方式,最新的openresty都在用ffi方式重构。而指令就是nginx配置,它会决定其中{}大括号内的代码在什么时候执行。这张图中,有初始nginx启动阶段、有rewrite/access阶段、有content阶段以及log阶段。这与我们之前的所说的11个阶段有什么关系呢?

我们来看这张图,有点复杂,最上面的绿框是nginx启动过程,其中黑色的框是master进程,而紫色的框是worker进程,中间的红点是钩子函数。中间的紫色框是worker进程在处理请求。最下面的绿色框是nginx在退出。可以看到,当nginx启动在,通过在配置文件中各第三方模块可以介入,在init_module回调函数实现东西也可以介入nginx的启动。当派生出worker子进程后,仍然可以通过回调init_master、init_process等回调方法介入启动过程。而实际处理请求时,先可以通过8个http阶段介入与请求的处理,在content阶段还可以使用排他性的r->content_handler(用于反向代理)来生成响应内容。在生成响应内容时,还可以通过init_upstream钩子函数决定选择哪一台上游服务器。生成响应内容后,通过filter过滤模块也可以介入请求的处理,最后在access log阶段也可以介入请求的处理。在nginx退出时仍然可以介入处理。

而openresty的指令就是像图中这么介入处理的。例如,rewrite_by_lua实际是在post_read阶段介入处理的,因为就像上面说过的,rewrite阶段都是官方模块在处理,所以openresty实际是在postread阶段,所以这个指令是相当靠前的。而balance_by_lua实际是在init_upstream钩子里介入的。

最后我们看一下nginx变量。有一类模块会生成新的nginx变量,它们通过处理http请求时定义的取值方法,生成了变量名对应的变量值,并以$符号或者lua中的ngx.var等方式提供给使用变量的模块。这些模块既包含C模块,也包括lua模块。C模块更关注高效,往往提供变量的模块都是C模块,而lua模块关注业务。所以,这两类语言最好的解耦方法就是使用变量。

最后我们看看第三部分nginx性能的优化。我们希望nginx可以把一台服务器的性能压榨到极致,主要从5个方面入手。首先是不能有长时占有CPU的代码段。因为nginx是事件驱动的、非阻塞的、异步架构代码,就像图中所示,nginx把本来操作系统应该做的事:切换不同的请求处理,改为在nginx进程内部处理了。怎么讲呢?传统的进程是同一时间只处理一个请求,所有处理请求的方法都是阻塞的,所以在处理完一个请求前不会处理下一个请求。因此,当大量并发请求存在时,意味着大量运行中的进程或者线程。操作系统希望最大化吞吐量,它就会切换不同的进程到CPU上执行,当一个进程因为阻塞请求导致的系统调用不满足时,例如读取磁盘转头磁头,就会被切换到内存中等待下次执行。而nginx采用的事件驱动,则是把这一过程放在nginx的用户态代码内了,首先用非阻塞系统调用检测到条件不满足,如果执行会导致操作系统执行进程间切换时,就会把该请求切到内存中等待下次执行,而nginx会选择条件满足的请求继续执行。因此,如果处理一个请求时消耗了大量的CPU时间,就会导致其他请求长时间得不到处理,以至于大量超时,形成恶性循环。所以,遇到某些第三方模块会大量消耗CPU时务必谨慎使用,真有这样的场景也不应当在nginx中做,可以用nginx反向代理到多线程应用中处理。因为操作系统会为每个进程分配5ms-800ms的时间片,它也会区分IO型或者CPU型进程,而上述进程是明显的CPU型进程,上下文切换不会很频繁。

第二个优化点就是减少上下文切换。在这页PPT中我们提到一个工具叫pidstat,它可以清晰的看到主动切换与被动切换。何谓主动切换呢?就是执行了某些阻塞式系统调用,当条件不满足时内核就会把进程切换出去,叫做cswch/s。而操作系统微观上串行宏观上并行实现的多任务,是使用抢占式内核实现的,它为每个进程分配时间片,时间片耗尽必须切出,这就叫nvswch/s。我们通过增加进程的静态优先级来增大时间片的大小。静态优先级分为40级,默认进程是0级,最大是-19,我们可以在nginx.conf里修改静态优先级。另外,还可以通过把worker进程绑定CPU,减少在多核服务器上的进程间切换代价。对于主动切换,则需要减少使用类似nginx模块的场景。有时这很难避免,例如读取静态文件,当频繁读取的内容打破内存缓存时,使用nio或者sendfile也没有用,仍然退化为阻塞式调用,此时用threadpool线程池就很有意义了,官方有个博客上提到此种场景下线程池有9倍的性能提升。当然,目前线程池只能用于读取静态资源。

第三个优化是减少内存的使用。很多并发的连接是不活跃的,但它们还是会在内核态、用户态占有大量的内存,而总内存其实很有限,所以我们的内存大小及各种内存相关配置影响了我们的并发量。先从连接谈起,在Nginx进程内为每个连接会分配一个ngx_connection_t结构体,每个ngx_connection_t各分配一个ngx_event_t结构体用作读、写事件,在64位操作系统下以上结构体每个连接(无论是TCP还是UDP)消耗的内存是232+96*2字节。在操作系统内核中,为了处理复杂的TCP协议,必须分配读、写缓冲用于进程的读写、滑动窗口、拥塞窗口等相关的协议收发,而linux为了高效使用内存,设立了普通模式和压力模式,即内存宽裕情况下为每个连接多分配一些缓存以提高吞吐量,在压力模式下则每个连接少分配一些缓存以提高并发连接数,这是通过tcp_moderate_rcvbuf开关控制的,而调整幅度可通过tcp_adv_win_scale控制,调整区间在读写缓存上设置。Nginx中含有大量内存池,形如*_pool_size都是在控制初始内存分配,即必须分配出去的内存。还有一类分配如8 buffer等,它们使用ngx_buffers_t结构体存储。共享内存是用于跨worker进程通讯的,而openresty里的share_dict就是通过共享内存实现的,当然使用共享内存通常要用slab伙伴系统管理内存块,再用rbtree红黑树或者链表等数据结构管理实际的逻辑。Nginx中还会用到大量的hash表,比如存储server_names等,这里会定义桶大小和桶个数。

第四个是优化网络。我们先从TCP层面看,无非是读、写消息、建立与关闭连接等功能。如果是读消息,我们需要关注tcp_rmem设置缓存区的大小,需要关注初始拥塞窗口rwnd的大小以提升网络可以快速达到最优值。在nginx上还有许多控制读取到固定消息的超时时间,在读取上游服务发来的响应时还可以通过limit_rate限流。在发送消息时,同样可以设置tcp_wmem设置缓存区大小,通过iptables命令对cwnd来提升初始窗口,nodelay和nopush都是为了提升吞吐量的算法,当然它们的副产品就是牺牲了及时性增大了latency。总体来说对于大流量场景应该打开它们。当然nopush只对sendfile有效。当发送响应给客户端时,也可以通过limit_rate进行流控。作为服务器建立连接时,Tcp Fast Open技术可以在SYN包里就携带请求,这减少了一次RTT,但有可能带来反复收到相同包的情况,一般不打开;使用Defered可以减少nginx对某连接的唤醒次数,提升CPU使用效率;reuseport可以提高负载均衡效果,使多worker进程更好的协同工作;backlog可以增大半连接与全连接队列,特别是新连接很多而Nginx

最后是减少IO调用。这只对读取本地的静态资源有效,例如打开sendfile采用了零拷贝技术就减少了内存拷贝次数以及进程间上下文切换次数。

最后对今天的演讲做个总结。我前阵子听梁宁的30堂产品课,上面说到看待产品或者人都是从5个层面,我觉得很适用于nginx。首先从表面层。例如相亲时先对异性的长相、谈吐、衣着,而看nginx则是看它的配置文件格式、access.log日志格式、进程启动方式等。第二层是角色层,例如与一个同事沟通,那么他是HR或者是前端工程师,都会影响他的谈吐以及沟通方式。而nginx的角色层就是最开始提到的静态资源服务、反向代理、API服务器,这影响它的表现层。第三层是资源层,对人则是人脉资源、精神资源、知识结构等,而对nginx则是它的大量的第三方模块、社区等。第四则是能力圈,对人就是一个人的能力大小,对nginx就是上文提到的nginx的核心架构、模块化、设计思路、算法、容器等。第五层是最内核的存在感,对人则是什么状态能让人满足,对nginx则是它的设计意义,就是我们前面提到的把一台服务器的硬件能力使用到极限以提供强大的web服务能力。这里底层总是在影响着上层,所以当我们掌握了nginx的底层,无论上层怎么变都很容易理解。

OK最后我在这里做个广告,智链达是一家杭州的创业公司,是希望助力一个行业实现转型升级的互联网服务公司,目前各种岗位都在招聘,我们其实面向的是一个蓝海行业,希望各位有兴趣的同学考虑加入。行,那我今天的介绍到这。

我要回帖

更多关于 new分配的内存空间 的文章

 

随机推荐