精华内容
下载资源
问答
  • Nginx工作原理优化总结。

    万次阅读 多人点赞 2013-05-16 11:04:53
    NGINX以高性能的负载均衡器,缓存,和web服务器闻名,驱动了全球超过 40% 繁忙的网站。在大多数场景下,默认的 NGINX 和 Linux 设置可以很好的工作,但要达到最佳性能,有些时候必须做些调整。首先我们先了解其...

          NGINX以高性能的负载均衡器,缓存,和web服务器闻名,驱动了全球超过 40% 最繁忙的网站。在大多数场景下,默认的 NGINX 和 Linux 设置可以很好的工作,但要达到最佳性能,有些时候必须做些调整。首先我们先了解其工作原理。

     

    一.  Nginx的模块与工作原理


    Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。

    Nginx的模块从结构上分为核心模块、基础模块和第三方模块:

    核心模块:HTTP模块、EVENT模块和MAIL模块

    基础模块:HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块,

    第三方模块:HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块。

    用户根据自己的需要开发的模块都属于第三方模块。正是有了这么多模块的支撑,Nginx的功能才会如此强大。

    Nginx的模块从功能上分为如下三类。

    Handlers(处理器模块)。此类模块直接处理请求,并进行输出内容和修改headers信息等操作。Handlers处理器模块一般只能有一个。

    Filters (过滤器模块)。此类模块主要对其他处理器模块输出的内容进行修改操作,最后由Nginx输出。

    Proxies (代理类模块)。此类模块是Nginx的HTTP Upstream之类的模块,这些模块主要与后端一些服务比如FastCGI等进行交互,实现服务代理和负载均衡等功能。

    图1-1展示了Nginx模块常规的HTTP请求和响应的过程。

          

    Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。

    Nginx的模块直接被编译进Nginx,因此属于静态编译方式。启动Nginx后,Nginx的模块被自动加载,不像Apache,首先将模块编译为一个so文件,然后在配置文件中指定是否进行加载。在解析配置文件时,Nginx的每个模块都有可能去处理某个请求,但是同一个处理请求只能由一个模块来完成。 

     

     

    二.  Nginx的进程模型


    在工作方式上,Nginx分为单工作进程和多工作进程两种模式。在单工作进程模式下,除主进程外,还有一个工作进程,工作进程是单线程的;在多工作进程模式下,每个工作进程包含多个线程。Nginx默认为单工作进程模式。

    Nginx在启动后,会有一个master进程和多个worker进程。

    1、master进程:管理进程

      master进程主要用来管理worker进程,具体包括如下4个主要功能:
            (1)接收来自外界的信号。
            (2)向各worker进程发送信号。
            (3)监控woker进程的运行状态。
            (4)当woker进程退出后(异常情况下),会自动重新启动新的woker进程。

    用户交互接口:master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。

    重启work进程:我们要控制nginx,只需要通过kill向master进程发送信号就行了。比如kill -HUP pid,则是告诉nginx,从容地重启nginx,我们一般用这个信号来重启nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的。

    master进程在接收到HUP信号后是怎么做的呢?

    1)、首先master进程在接到信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以光荣退休了。

    2)、新的worker在启动后,就开始接收新的请求,而老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。

    直接给master进程发送信号,这是比较传统的操作方式,nginx在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如,./nginx -s reload,就是来重启nginx,./nginx -s stop,就是来停止nginx的运行。如何做到的呢?我们还是拿reload来说,我们看到,执行命令时,我们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道我们的目的是控制nginx来重新加载配置文件了,它会向master进程发送信号,然后接下来的动作,就和我们直接向master进程发送信号一样了。

    2、worker进程:处理请求

    而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。

    worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?

     Nginx采用异步非阻塞的方式来处理网络事件,类似于Libevent,具体过程如下:

    1)接收请求:首先,每个worker进程都是从master进程fork过来,在master进程建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,每个work进程都可以去accept这个socket(listenfd)。当一个client连接到来时,所有accept的work进程都会受到通知,但只有一个进程可以accept成功,其它的则会accept失败。为保证只有一个进程处理该连接,Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept连接。所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。

    2)处理请求:当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。

    我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。worker进程之间是平等的,每个进程,处理请求的机会也是一样的。

    nginx的进程模型,可以由下图来表示:

    三.   Nginx为啥性能高-多进程IO模型


          参考http://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=407889757&idx=3&sn=cfa8a70a5fd2a674a91076f67808273c&scene=23&srcid=0401aeJQEraSG6uvLj69Hfve#rd

    1、nginx采用多进程模型好处

          首先,对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多。

          其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程。当然,worker进程的异常退出,肯定是程序有bug了,异常退出,会导致当前worker上的所有请求失败,不过不会影响到所有请求,所以降低了风险。

    2、nginx多进程事件模型:异步非阻塞


             虽然nginx采用多worker的方式来处理请求,每个worker里面只有一个主线程,那能够处理的并发数很有限啊,多少个worker就能处理多少个并发,何来高并发呢?非也,这就是nginx的高明之处,nginx采用了异步非阻塞的方式来处理请求,也就是说,nginx是可以同时处理成千上万个请求的。一个worker进程可以同时处理的请求数只受限于内存大小,而且在架构设计上,不同的worker进程之间处理并发请求时几乎没有同步锁的限制,worker进程通常不会进入睡眠状态,因此,当Nginx上的进程数与CPU核心数相等时(最好每一个worker进程都绑定特定的CPU核心),进程间切换的代价是最小的。

           而apache的常用工作方式(apache也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用),每个进程在一个时刻只处理一个请求因此,当并发数上到几千时,就同时有几千的进程在处理请求了。这对操作系统来说,是个不小的挑战,进程带来的内存占用非常大,进程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。

         

     

             为什么nginx可以采用异步非阻塞的方式来处理呢,或者异步非阻塞到底是怎么回事呢?具体请看:使用 libevent 和 libev 提高网络应用性能——I/O模型演进变化史

             我们先回到原点,看看一个请求的完整过程:首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。

             具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢,cpu空闲下来没人用,cpu利用率自然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有什么区别,注意,别增加无谓的上下文切换。所以,在nginx里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,马上返回EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就可以先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查一下事件的状态,你可以做更多的事情了,但带来的开销也是不小的。

    关于IO模型:http://blog.csdn.net/hguisu/article/details/7453390

           nginx支持的事件模型如下(nginx的wiki):

           Nginx支持如下处理连接的方法(I/O复用方法),这些方法可以通过use指令指定。

    • select– 标准方法。 如果当前平台没有更有效的方法,它是编译时默认的方法。你可以使用配置参数 –with-select_module 和 –without-select_module 来启用或禁用这个模块。
    • poll– 标准方法。 如果当前平台没有更有效的方法,它是编译时默认的方法。你可以使用配置参数 –with-poll_module 和 –without-poll_module 来启用或禁用这个模块。
    • kqueue– 高效的方法,使用于 FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X. 使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。
    • epoll 高效的方法,使用于Linux内核2.6版本及以后的系统。在某些发行版本中,如SuSE 8.2, 有让2.4版本的内核支持epoll的补丁。
    • rtsig 可执行的实时信号,使用于Linux内核版本2.2.19以后的系统。默认情况下整个系统中不能出现大于1024个POSIX实时(排队)信号。这种情况 对于高负载的服务器来说是低效的;所以有必要通过调节内核参数 /proc/sys/kernel/rtsig-max 来增加队列的大小。可是从Linux内核版本2.6.6-mm2开始, 这个参数就不再使用了,并且对于每个进程有一个独立的信号队列,这个队列的大小可以用 RLIMIT_SIGPENDING 参数调节。当这个队列过于拥塞,nginx就放弃它并且开始使用 poll 方法来处理连接直到恢复正常。
    • /dev/poll – 高效的方法,使用于 Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+.
    • eventport – 高效的方法,使用于 Solaris 10. 为了防止出现内核崩溃的问题, 有必要安装这个 安全补丁。

            在linux下面,只有epoll是高效的方法

            下面再来看看epoll到底是如何高效的
           Epoll是Linux内核为处理大批量句柄而作了改进的poll。 要使用epoll只需要这三个系统调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),在2.6内核中得到广泛应用。

            epoll的优点

    • 支持一个进程打开大数目的socket描述符(FD)

            select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

    • IO效率不随FD数目增加而线性下降

             传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的,但 是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操 作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

    • 使用mmap加速内核与用户空间的消息传递。

            这 点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很 重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。

    • 内核微调

             这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协 议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小— 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。

        (epoll内容,参考epoll_互动百科)

          推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。像这种小的优化在nginx中非常常见,同时也说明了nginx作者的苦心孤诣。比如,nginx在做4个字节的字符串比较时,会将4个字符转换成一个int型,再作比较,以减少cpu的指令数等等。

    代码来总结一下nginx的事件处理模型:

     

    while (true) {
        for t in run_tasks:
            t.handler();
        update_time(&now);
        timeout = ETERNITY;
        for t in wait_tasks: /* sorted already */
            if (t.time <= now) {
                t.timeout_handler();
            } else {
                timeout = t.time - now;
                break;
            }
        nevents = poll_function(events, timeout);
        for i in nevents:
            task t;
            if (events[i].type == READ) {
                t.handler = read_handler;
            } else { /* events[i].type == WRITE */
                t.handler = write_handler;
            }
            run_tasks_add(t);
    }

     

     

     

    四.  Nginx+FastCGI运行原理


    1、什么是 FastCGI

    FastCGI是一个可伸缩地、高速地在HTTP server和动态脚本语言间通信的接口。多数流行的HTTP server都支持FastCGI,包括Apache、Nginx和lighttpd等。同时,FastCGI也被许多脚本语言支持,其中就有PHP。

    FastCGI是从CGI发展改进而来的。传统CGI接口方式的主要缺点是性能很差,因为每次HTTP服务器遇到动态程序时都需要重新启动脚本解析器来执行解析,然后将结果返回给HTTP服务器。这在处理高并发访问时几乎是不可用的。另外传统的CGI接口方式安全性也很差,现在已经很少使用了。

    FastCGI接口方式采用C/S结构,可以将HTTP服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或者多个脚本解析守护进程。当HTTP服务器每次遇到动态程序时,可以将其直接交付给FastCGI进程来执行,然后将得到的结果返回给浏览器。这种方式可以让HTTP服务器专一地处理静态请求或者将动态脚本服务器的结果返回给客户端,这在很大程度上提高了整个应用系统的性能。

    2、Nginx+FastCGI运行原理

    Nginx不支持对外部程序的直接调用或者解析,所有的外部程序(包括PHP)必须通过FastCGI接口来调用。FastCGI接口在Linux下是socket(这个socket可以是文件socket,也可以是ip socket)。

    wrapper:为了调用CGI程序,还需要一个FastCGI的wrapper(wrapper可以理解为用于启动另一个程序的程序),这个wrapper绑定在某个固定socket上,如端口或者文件socket。当Nginx将CGI请求发送给这个socket的时候,通过FastCGI接口,wrapper接收到请求,然后Fork(派生)出一个新的线程,这个线程调用解释器或者外部程序处理脚本并读取返回数据;接着,wrapper再将返回的数据通过FastCGI接口,沿着固定的socket传递给Nginx;最后,Nginx将返回的数据(html页面或者图片)发送给客户端。这就是Nginx+FastCGI的整个运作过程,如图1-3所示。

           

     

          所以,我们首先需要一个wrapper,这个wrapper需要完成的工作:

    1. 通过调用fastcgi(库)的函数通过socket和ningx通信(读写socket是fastcgi内部实现的功能,对wrapper是非透明的)
    2. 调度thread,进行fork和kill
    3. 和application(php)进行通信

    3、spawn-fcgi与PHP-FPM

           FastCGI接口方式在脚本解析服务器上启动一个或者多个守护进程对动态脚本进行解析,这些进程就是FastCGI进程管理器,或者称为FastCGI引擎。 spawn-fcgi与PHP-FPM就是支持PHP的两个FastCGI进程管理器。因此HTTPServer完全解放出来,可以更好地进行响应和并发处理。

           spawn-fcgi与PHP-FPM的异同:
           1)spawn-fcgi是HTTP服务器lighttpd的一部分,目前已经独立成为一个项目,一般与lighttpd配合使用来支持PHP。但是ligttpd的spwan-fcgi在高并发访问的时候,会出现内存泄漏甚至自动重启FastCGI的问题。即:PHP脚本处理器当机,这个时候如果用户访问的话,可能就会出现白页(即PHP不能被解析或者出错)。

           2)Nginx是个轻量级的HTTP server,必须借助第三方的FastCGI处理器才可以对PHP进行解析,因此其实这样看来nginx是非常灵活的,它可以和任何第三方提供解析的处理器实现连接从而实现对PHP的解析(nginx.conf中很容易设置)nginx也可以使用spwan-fcgi(需要一同安装lighttpd,但是需要为nginx避开端口,一些较早的blog有这方面安装的教程),但是由于spawn-fcgi具有上面所述的用户逐渐发现的缺陷,现在慢慢减少用nginx+spawn-fcgi组合了。

           由于spawn-fcgi的缺陷,现在出现了第三方(目前已经加入到PHP core中)的PHP的FastCGI处理器PHP-FPM,它和spawn-fcgi比较起来有如下优点:

           由于它是作为PHP的patch补丁来开发的,安装的时候需要和php源码一起编译,也就是说编译到php core中了,因此在性能方面要优秀一些;

    同时它在处理高并发方面也优于spawn-fcgi,至少不会自动重启fastcgi处理器。因此,推荐使用Nginx+PHP/PHP-FPM这个组合对PHP进行解析。

          相对Spawn-FCGI,PHP-FPM在CPU和内存方面的控制都更胜一筹,而且前者很容易崩溃,必须用crontab进行监控,而PHP-FPM则没有这种烦恼。
           FastCGI 的主要优点是把动态语言和HTTP Server分离开来,所以Nginx与PHP/PHP-FPM经常被部署在不同的服务器上,以分担前端Nginx服务器的压力,使Nginx专一处理静态请求和转发动态请求,而PHP/PHP-FPM服务器专一解析PHP动态请求。

    4、Nginx+PHP-FPM

          PHP-FPM是管理FastCGI的一个管理器,它作为PHP的插件存在,在安装PHP要想使用PHP-FPM时在老php的老版本(php5.3.3之前)就需要把PHP-FPM以补丁的形式安装到PHP中,而且PHP要与PHP-FPM版本一致,这是必须的)

       PHP-FPM其实是PHP源代码的一个补丁,旨在将FastCGI进程管理整合进PHP包中。必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。
       PHP5.3.3已经集成php-fpm了,不再是第三方的包了。PHP-FPM提供了更好的PHP进程管理方式,可以有效控制内存和进程、可以平滑重载PHP配置,比spawn-fcgi具有更多优点,所以被PHP官方收录了。在./configure的时候带 –enable-fpm参数即可开启PHP-FPM。

          fastcgi已经在php5.3.5的core中了,不必在configure时添加 --enable-fastcgi了。老版本如php5.2的需要加此项。

          当我们安装Nginx和PHP-FPM完后,配置信息:

         PHP-FPM的默认配置php-fpm.conf:

         listen_address  127.0.0.1:9000 #这个表示php的fastcgi进程监听的ip地址以及端口

          start_servers

          min_spare_servers

          max_spare_servers

          Nginx配置运行php: 编辑nginx.conf加入如下语句:

          location ~ \.php$ {
                root html;   
                fastcgi_pass 127.0.0.1:9000; 指定了fastcgi进程侦听的端口,nginx就是通过这里与php交互的
                fastcgi_index index.php;
                include fastcgi_params;
                 fastcgi_param SCRIPT_FILENAME   /usr/local/nginx/html$fastcgi_script_name;
        }

        Nginx通过location指令,将所有以php为后缀的文件都交给127.0.0.1:9000来处理,而这里的IP地址和端口就是FastCGI进程监听的IP地址和端口。

         其整体工作流程:

         1)、FastCGI进程管理器php-fpm自身初始化,启动主进程php-fpm和启动start_servers个CGI 子进程。

               主进程php-fpm主要是管理fastcgi子进程,监听9000端口。

               fastcgi子进程等待来自Web Server的连接。

         2)、当客户端请求到达Web Server Nginx是时,Nginx通过location指令,将所有以php为后缀的文件都交给127.0.0.1:9000来处理,即Nginx通过location指令,将所有以php为后缀的文件都交给127.0.0.1:9000来处理。

          3)FastCGI进程管理器PHP-FPM选择并连接到一个子进程CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程。

          4)、FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。

          5)、FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在 WebServer中)的下一个连接。

     

    五.   Nginx+PHP正确配置


    一般web都做统一入口:把PHP请求都发送到同一个文件上,然后在此文件里通过解析「REQUEST_URI」实现路由。

    Nginx配置文件分为好多块,常见的从外到内依次是「http」、「server」、「location」等等,缺省的继承关系是从外到内,也就是说内层块会自动获取外层块的值作为缺省值。

    例如:

    server {
        listen 80;
        server_name foo.com;
        root /path;
        location / {
            index index.html index.htm index.php;
            if (!-e $request_filename) {
                rewrite . /index.php last;
            }
        }
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME /path$fastcgi_script_name;
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
        }
    } 

    1)  不应该在location 模块定义index

    一旦未来需要加入新的「location」,必然会出现重复定义的「index」指令,这是因为多个「location」是平级的关系,不存在继承,此时应该在「server」里定义「index」,借助继承关系,「index」指令在所有的「location」中都能生效。

    2)     使用try_files

    接下来看看「if」指令,说它是大家误解最深的Nginx指令毫不为过:

    if (!-e $request_filename) {
        rewrite . /index.php last;
    }

    很多人喜欢用「if」指令做一系列的检查,不过这实际上是「try_files」指令的职责:

    try_files $uri $uri/ /index.php;

    除此以外,初学者往往会认为「if」指令是内核级的指令,但是实际上它是rewrite模块的一部分,加上Nginx配置实际上是声明式的,而非过程式的,所以当其和非rewrite模块的指令混用时,结果可能会非你所愿。

     

    3)fastcgi_params」配置文件

    include fastcgi_params;

    Nginx有两份fastcgi配置文件,分别是「fastcgi_params」和「fastcgi.conf」,它们没有太大的差异,唯一的区别是后者比前者多了一行「SCRIPT_FILENAME」的定义:

    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    注意:$document_root 和 $fastcgi_script_name 之间没有 /。

    原本Nginx只有「fastcgi_params」,后来发现很多人在定义「SCRIPT_FILENAME」时使用了硬编码的方式,于是为了规范用法便引入了「fastcgi.conf」。

     

    不过这样的话就产生一个疑问:为什么一定要引入一个新的配置文件,而不是修改旧的配置文件?这是因为「fastcgi_param」指令是数组型的,和普通指令相同的是:内层替换外层;和普通指令不同的是:当在同级多次使用的时候,是新增而不是替换。换句话说,如果在同级定义两次「SCRIPT_FILENAME」,那么它们都会被发送到后端,这可能会导致一些潜在的问题,为了避免此类情况,便引入了一个新的配置文件。

    此外,我们还需要考虑一个安全问题:在PHP开启「cgi.fix_pathinfo」的情况下,PHP可能会把错误的文件类型当作PHP文件来解析。如果Nginx和PHP安装在同一台服务器上的话,那么最简单的解决方法是用「try_files」指令做一次过滤:

     

    try_files $uri =404;

    依照前面的分析,给出一份改良后的版本,是不是比开始的版本清爽了很多:

    server {
        listen 80;
        server_name foo.com;
        root /path;
        index index.html index.htm index.php;
        location / {
            try_files $uri $uri/ /index.php;
        }
        location ~ \.php$ {
           try_files $uri =404;
           include fastcgi.conf;
           fastcgi_pass 127.0.0.1:9000;
       }
    }

     

     

     

     

    六.   Nginx优化


     1. 编译安装过程优化

    1).减小Nginx编译后的文件大小

    在编译Nginx时,默认以debug模式进行,而在debug模式下会插入很多跟踪和ASSERT之类的信息,编译完成后,一个Nginx要有好几兆字节。而在编译前取消Nginx的debug模式,编译完成后Nginx只有几百千字节。因此可以在编译之前,修改相关源码,取消debug模式。具体方法如下:

    在Nginx源码文件被解压后,找到源码目录下的auto/cc/gcc文件,在其中找到如下几行:

     # debug  
    CFLAGS=”$CFLAGS -g” 

    注释掉或删掉这两行,即可取消debug模式。

    2.为特定的CPU指定CPU类型编译优化

    在编译Nginx时,默认的GCC编译参数是“-O”,要优化GCC编译,可以使用以下两个参数:

    1. --with-cc-opt='-O3' 
    2. --with-cpu-opt=CPU  #为特定的 CPU 编译,有效的值包括:
      pentium, pentiumpro, pentium3, # pentium4, athlon, opteron, amd64, sparc32, sparc64, ppc64 

    要确定CPU类型,可以通过如下命令: #cat /proc/cpuinfo | grep "model name" 

     

    2. 利用TCMalloc优化Nginx的性能

    TCMalloc的全称为Thread-Caching Malloc,是谷歌开发的开源工具google-perftools中的一个成员。与标准的glibc库的Malloc相比,TCMalloc库在内存分配效率和速度上要高很多,这在很大程度上提高了服务器在高并发情况下的性能,从而降低了系统的负载。下面简单介绍如何为Nginx添加TCMalloc库支持。

    要安装TCMalloc库,需要安装libunwind(32位操作系统不需要安装)和google-perftools两个软件包,libunwind库为基于64位CPU和操作系统的程序提供了基本函数调用链和函数调用寄存器功能。下面介绍利用TCMalloc优化Nginx的具体操作过程。

    1).安装libunwind库

    可以从http://download.savannah.gnu.org/releases/libunwind下载相应的libunwind版本,这里下载的是libunwind-0.99-alpha.tar.gz。安装过程如下

    #tar zxvf libunwind-0.99-alpha.tar.gz  
    # cd libunwind-0.99-alpha/  
    #CFLAGS=-fPIC ./configure  
    #make CFLAGS=-fPIC  
    #make CFLAGS=-fPIC install

    2).安装google-perftools

    可以从http://google-perftools.googlecode.com下载相应的google-perftools版本,这里下载的是google-perftools-1.8.tar.gz。安装过程如下:

    [root@localhost home]#tar zxvf google-perftools-1.8.tar.gz  
    [root@localhost home]#cd google-perftools-1.8/  
    [root@localhost google-perftools-1.8]# ./configure  
    [root@localhost google-perftools-1.8]#make && make install  
    [root@localhost google-perftools-1.8]#echo "/usr/
    local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf  
    [root@localhost google-perftools-1.8]# ldconfig

    至此,google-perftools安装完成。

    3).重新编译Nginx

    为了使Nginx支持google-perftools,需要在安装过程中添加“–with-google_perftools_module”选项重新编译Nginx。安装代码如下:

    [root@localhostnginx-0.7.65]#./configure \  
    >--with-google_perftools_module --with-http_stub_status_module  --prefix=/opt/nginx  
    [root@localhost nginx-0.7.65]#make  
    [root@localhost nginx-0.7.65]#make install

    到这里Nginx安装完成。

    4).为google-perftools添加线程目录

    创建一个线程目录,这里将文件放在/tmp/tcmalloc下。操作如下:

    [root@localhost home]#mkdir /tmp/tcmalloc  
    [root@localhost home]#chmod 0777 /tmp/tcmalloc

    5).修改Nginx主配置文件

    修改nginx.conf文件,在pid这行的下面添加如下代码:

    #pid        logs/nginx.pid;  
    google_perftools_profiles /tmp/tcmalloc;

    接着,重启Nginx即可完成google-perftools的加载。

    6).验证运行状态

    为了验证google-perftools已经正常加载,可通过如下命令查看:

    [root@ localhost home]# lsof -n | grep tcmalloc  
    nginx      2395 nobody   9w  REG    8,8       0    1599440 /tmp/tcmalloc.2395  
    nginx      2396 nobody   11w REG   8,8       0    1599443 /tmp/tcmalloc.2396  
    nginx      2397 nobody   13w REG  8,8        0    1599441  /tmp/tcmalloc.2397  
    nginx     2398 nobody    15w REG  8,8     0    1599442 /tmp/tcmalloc.2398

    由于在Nginx配置文件中设置worker_processes的值为4,因此开启了4个Nginx线程,每个线程会有一行记录。每个线程文件后面的数字值就是启动的Nginx的pid值。

    至此,利用TCMalloc优化Nginx的操作完成。

    3.Nginx内核参数优化

    内核参数的优化,主要是在Linux系统中针对Nginx应用而进行的系统内核参数优化。

    下面给出一个优化实例以供参考。

    net.ipv4.tcp_max_tw_buckets = 6000
    net.ipv4.ip_local_port_range = 1024 65000  
    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_syncookies = 1
    net.core.somaxconn = 262144
    net.core.netdev_max_backlog = 262144
    net.ipv4.tcp_max_orphans = 262144
    net.ipv4.tcp_max_syn_backlog = 262144
    net.ipv4.tcp_synack_retries = 1
    net.ipv4.tcp_syn_retries = 1
    net.ipv4.tcp_fin_timeout = 1
    net.ipv4.tcp_keepalive_time = 30

     

    将上面的内核参数值加入/etc/sysctl.conf文件中,然后执行如下命令使之生效:

    [root@ localhost home]#/sbin/sysctl -p 

    下面对实例中选项的含义进行介绍:

    TCP参数设置:

    net.ipv4.tcp_max_tw_buckets :选项用来设定timewait的数量,默认是180 000,这里设为6000。

    net.ipv4.ip_local_port_range:选项用来设定允许系统打开的端口范围。在高并发情况否则端口号会不够用。当NGINX充当代理时,每个到上游服务器的连接都使用一个短暂或临时端口。

    net.ipv4.tcp_tw_recycle:选项用于设置启用timewait快速回收.

    net.ipv4.tcp_tw_reuse:选项用于设置开启重用,允许将TIME-WAIT sockets重新用于新的TCP连接。

    net.ipv4.tcp_syncookies:选项用于设置开启SYN Cookies,当出现SYN等待队列溢出时,启用cookies进行处理。

    net.ipv4.tcp_max_orphans:选项用于设定系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤立连接将立即被复位并打印出警告信息。这个限制只是为了防止简单的DoS攻击。不能过分依靠这个限制甚至人为减小这个值,更多的情况下应该增加这个值。

    net.ipv4.tcp_max_syn_backlog:选项用于记录那些尚未收到客户端确认信息的连接请求的最大值。对于有128MB内存的系统而言,此参数的默认值是1024,对小内存的系统则是128。

    net.ipv4.tcp_synack_retries参数的值决定了内核放弃连接之前发送SYN+ACK包的数量。

    net.ipv4.tcp_syn_retries选项表示在内核放弃建立连接之前发送SYN包的数量。

    net.ipv4.tcp_fin_timeout选项决定了套接字保持在FIN-WAIT-2状态的时间。默认值是60秒。正确设置这个值非常重要,有时即使一个负载很小的Web服务器,也会出现大量的死套接字而产生内存溢出的风险。

    net.ipv4.tcp_syn_retries选项表示在内核放弃建立连接之前发送SYN包的数量。

    如果发送端要求关闭套接字,net.ipv4.tcp_fin_timeout选项决定了套接字保持在FIN-WAIT-2状态的时间。接收端可以出错并永远不关闭连接,甚至意外宕机。

    net.ipv4.tcp_fin_timeout的默认值是60秒。需要注意的是,即使一个负载很小的Web服务器,也会出现因为大量的死套接字而产生内存溢出的风险。FIN-WAIT-2的危险性比FIN-WAIT-1要小,因为它最多只能消耗1.5KB的内存,但是其生存期长些。

    net.ipv4.tcp_keepalive_time选项表示当keepalive启用的时候,TCP发送keepalive消息的频度。默认值是2(单位是小时)。

     

    缓冲区队列:

    net.core.somaxconn:选项的默认值是128, 这个参数用于调节系统同时发起的tcp连接数,在高并发的请求中,默认的值可能会导致链接超时或者重传,因此,需要结合并发请求数来调节此值。

     由NGINX可接受的数目决定。默认值通常很低,但可以接受,因为NGINX 接收连接非常快,但如果网站流量大时,就应该增加这个值。内核日志中的错误消息会提醒这个值太小了,把值改大,直到错误提示消失。
    注意: 如果设置这个值大于512,相应地也要改变NGINX listen指令的backlog参数。

    net.core.netdev_max_backlog:选项表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许发送到队列的数据包的最大数目。

     

    4.PHP-FPM的优化

    如果您高负载网站使用PHP-FPM管理FastCGI,这些技巧也许对您有用:

    1)增加FastCGI进程数

    把PHP FastCGI子进程数调到100或以上,在4G内存的服务器上200就可以建议通过压力测试获取最佳值。

    2)增加 PHP-FPM打开文件描述符的限制

    标签rlimit_files用于设置PHP-FPM对打开文件描述符的限制,默认值为1024。这个标签的值必须和Linux内核打开文件数关联起来,例如,要将此值设置为65 535,就必须在Linux命令行执行“ulimit -HSn 65536”。

           然后 增加 PHP-FPM打开文件描述符的限制:
         # vi /path/to/php-fpm.conf
        找到
    “<valuename="rlimit_files">1024</value>”
    把1024更改为 4096或者更高
    .
    重启 PHP-FPM.

           ulimit -n 要调整为65536甚至更大。如何调这个参数,可以参考网上的一些文章。命令行下执行 ulimit -n 65536即可修改。如果不能修改,需要设置  /etc/security/limits.conf,加入

    * hard nofile65536

    * soft nofile 65536

             3)适当增加max_requests

        标签max_requests指明了每个children最多处理多少个请求后便会被关闭,默认的设置是500。

        <value name="max_requests"> 500 </value>

    5.nginx.conf的参数优化

    nginx要开启的进程数 一般等于cpu的总核数 其实一般情况下开4个或8个就可以。

        每个nginx进程消耗的内存10兆的模样

    worker_cpu_affinity
        仅适用于linux,使用该选项可以绑定worker进程和CPU(2.4内核的机器用不了)

        假如是8 cpu 分配如下:
        worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000

    00100000 01000000 10000000

        nginx可以使用多个worker进程,原因如下:

    to use SMP 
    to decrease latency when workers blockend on disk I/O 
    to limit number of connections per process when select()/poll() is

    used The worker_processes and worker_connections from the event sections

    allows you to calculate maxclients value: k max_clients = worker_processes * worker_connections

    worker_rlimit_nofile 102400;

        每个nginx进程打开文件描述符最大数目 配置要和系统的单进程打开文件数一致,linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535 nginx调度时分配请求到进程并不是那么的均衡,假如超过会返回502错误。我这里写的大一点

    use epoll

        Nginx使用了最新的epoll(Linux 2.6内核)和kqueue(freebsd)网络I/O模型,而Apache则使用的是传统的select模型。

    处理大量的连接的读写,Apache所采用的select网络I/O模型非常低效。在高并发服务器中,轮询I/O是最耗时间的操作 目前Linux下能够承受高并发

        访问的Squid、Memcached都采用的是epoll网络I/O模型。

    worker_processes 

        NGINX工作进程数(默认值是1)。在大多数情况下,一个CPU内核运行一个工作进程最好,建议将这个指令设置成自动就可以。有时可能想增大这个值,比如当工作进程需要做大量的磁盘I/O。

    worker_connections 65535;
       每个工作进程允许最大的同时连接数 (Maxclient = work_processes * worker_connections)

    keepalive_timeout 75

        keepalive超时时间

        这里需要注意官方的一句话:
    The parameters can differ from each other. Line Keep-Alive:

    timeout=time understands Mozilla and Konqueror. MSIE itself shuts

    keep-alive connection approximately after 60 seconds.

     

    client_header_buffer_size 16k
    large_client_header_buffers 4 32k

    客户请求头缓冲大小 
    nginx默认会用client_header_buffer_size这个buffer来读取header值,如果header过大,它会使用large_client_header_buffers来读取

    如果设置过小HTTP头/Cookie过大 会报400 错误 nginx 400 bad request
    求行如果超过buffer,就会报HTTP 414错误(URI Too Long) nginx接受最长的HTTP头部大小必须比其中一个buffer大,否则就会报400的HTTP错误(Bad Request)。

    open_file_cache max 102400

    使用字段:http, server, location 这个指令指定缓存是否启用,如果启用,将记录文件以下信息: ·打开的文件描述符,大小信息和修改时间. ·存在的目录信息. ·在搜索文件过程中的错误信息 -- 没有这个文件,无法正确读取,参考open_file_cache_errors 指令选项:
    ·max - 指定缓存的最大数目,如果缓存溢出,最长使用过的文件(LRU)将被移除
    例: open_file_cache max=1000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;

    open_file_cache_errors
    语法:open_file_cache_errors on | off 默认值:open_file_cache_errors off 使用字段:http, server, location 这个指令指定是否在搜索一个文件是记录cache错误.

    open_file_cache_min_uses

    语法:open_file_cache_min_uses number 默认值:open_file_cache_min_uses 1 使用字段:http, server, location 这个指令指定了在open_file_cache指令无效的参数中一定的时间范围内可以使用的最小文件数,如 果使用更大的值,文件描述符在cache中总是打开状态.
    open_file_cache_valid

    语法:open_file_cache_valid time 默认值:open_file_cache_valid 60 使用字段:http, server, location 这个指令指定了何时需要检查open_file_cache中缓存项目的有效信息.


    开启gzip
    gzip on;
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_http_version 1.0;
    gzip_comp_level 2;
    gzip_types text/plain application/x-javascript text/css

    application/xml;
    gzip_vary on;

    缓存静态文件:

    location ~* ^.+\.(swf|gif|png|jpg|js|css)$ {
        root /usr/local/ku6/ktv/show.ku6.com/;
        expires 1m;
    }

     

    响应缓冲区:

    比如我们Nginx+Tomcat 代理访问JS无法完全加载,这几个参数影响:

    proxy_buffer_size 128k;
    proxy_buffers   32 128k;
    proxy_busy_buffers_size 128k;

    Nginx在代理了相应服务后或根据我们配置的UpStream和location来获取相应的文件,首先文件会被解析到nginx的内存或者临时文件目录中,然后由nginx再来响应。那么当proxy_buffers和proxy_buffer_size以及proxy_busy_buffers_size 都太小时,会将内容根据nginx的配置生成到临时文件中,但是临时文件的大小也有默认值。所以当这四个值都过小时就会导致部分文件只加载一部分。所以要根据我们的服务器情况适当的调整proxy_buffers和proxy_buffer_size以及proxy_busy_buffers_size、proxy_temp_file_write_size。具体几个参数的详细如下:

    proxy_buffers   32 128k;  设置了32个缓存区,每个的大小是128k

    proxy_buffer_size 128k; 每个缓存区的大小是128k,当两个值不一致时没有找到具体哪个有效,建议和上面的设置一致。

    proxy_busy_buffers_size 128k;设置使用中的缓存区的大小,控制传输至客户端的缓存的最大

    proxy_temp_file_write_size 缓存文件的大小

     

    6.优化访问日志

        记录每个请求会消耗CPU和I/O周期,一种降低这种影响的方式是缓冲访问日志。使用缓冲,而不是每条日志记录都单独执行写操作,NGINX会缓冲一连串的日志记录,使用单个操作把它们一起写到文件中。
        要启用访问日志的缓存,就涉及到在access_log指令中buffer=size这个参数。当缓冲区达到size值时,NGINX会把缓冲区的内容写到日志中。让NGINX在指定的一段时间后写缓存,就包含flush=time参数。当两个参数都设置了,当下个日志条目超出缓冲区值或者缓冲区中日志条目存留时间超过设定的时间值,NGINX都会将条目写入日志文件。当工作进程重新打开它的日志文件或退出时,也会记录下来。要完全禁用访问日志记录的功能,将access_log 指令设置成off参数。

     

    7.限流

    你可以设置多个限制,防止用户消耗太多的资源,避免影响系统性能和用户体验及安全。 以下是相关的指令:
    limit_conn and limit_conn_zone:NGINX接受客户连接的数量限制,例如单个IP地址的连接。设置这些指令可以防止单个用户打开太多的连接,消耗超出自己的资源。
    limit_rate:传输到客户端响应速度的限制(每个打开多个连接的客户消耗更多的带宽)。设置这个限制防止系统过载,确保所有客户端更均匀的服务质量。
    limit_req and limit_req_zone:NGINX处理请求的速度限制,与limit_rate有相同的功能。可以提高安全性,尤其是对登录页面,通过对用户限制请求速率设置一个合理的值,避免太慢的程序覆盖你的应用请求(比如DDoS攻击)。
    max_conns:上游配置块中服务器指令参数。在上游服务器组中单个服务器可接受最大并发数量。使用这个限制防止上游服务器过载。设置值为0(默认值)表示没有限制。
    queue (NGINX Plus) :创建一个队列,用来存放在上游服务器中超出他们最大max_cons限制数量的请求。这个指令可以设置队列请求的最大值,还可以选择设置在错误返回之前最大等待时间(默认值是60秒)。如果忽略这个指令,请求不会放入队列。

     

     

     

     

    七.   错误排查


    所有错误的都在错误文件error.log里面

    1、400 bad request错误的原因和解决办法

         配置nginx.conf相关设置如下.

         client_header_buffer_size 16k;
         large_client_header_buffers 4 64k;

         根据具体情况调整,一般适当调整值就可以。

    2、Nginx出现的413 Request Entity Too Large错误

             这个错误一般在上传文件的时候会出现,

            client intended to send too large body: 23937077 bytes, client: 10.45.142.41, server: localhost, request

            服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。
      如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

             编辑Nginx主配置文件Nginx.conf,找到http{}段,添加

            client_max_body_size 10m; //设置多大根据自己的需求作调整.

            如果运行php的话这个大小client_max_body_size要和php.ini中的如下值的最大值一致或者稍大,这样就不会因为提交数据大         小不一致出现的错误。

            post_max_size = 10M
            upload_max_filesize = 2M

     

    1、Nginx 502 Bad Gateway:

    作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。

    常见原因:
    1、后端服务挂了的情况,直接502 (nginx error日志:connect() failed (111: Connection refused) )
    2、后端服务在重启
    实例:将后端服务关掉,然后向nginx发送请求后端接口,nginx日志可以看到502错误。

    如果nginx+php出现502, 错误分析:

    php-cgi进程数不够用、php执行时间长(mysql慢)、或者是php-cgi进程死掉,都会出现502错误

    一般来说Nginx 502 Bad Gateway和php-fpm.conf的设置有关,而Nginx 504 Gateway Time-out则是与nginx.conf的设置有关

    1)、查看当前的PHP FastCGI进程数是否够用:

    netstat -anpo | grep "php-cgi" | wc -l

      如果实际使用的“FastCGI进程数”接近预设的“FastCGI进程数”,那么,说明“FastCGI进程数”不够用,需要增大。

    2)、部分PHP程序的执行时间超过了Nginx的等待时间,可以适当增加

         nginx.conf配置文件中FastCGI的timeout时间,例如:

    http {
        ......
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        ......
    }

    2、504 Gateway Timeout :

    nginx作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)收到响应。

    常见原因:
    该接口太耗时,后端服务接收到请求,开始执行,未能在设定时间返回数据给nginx
    后端服务器整体负载太高,接受到请求之后,由于线程繁忙,未能安排给请求的接口,导致未能在设定时间返回数据给nginx

     

    2、413 Request Entity Too Large
       

        解决:增大client_max_body_size

        client_max_body_size:指令指定允许客户端连接的最大请求实体大小,它出现在请求头部的Content-Length字段. 如果请求大于指定的值,客户端将收到一个"Request Entity Too Large" (413)错误. 记住,浏览器并不知道怎样显示这个错误.

        php.ini中增大
    post_max_size 和upload_max_filesize

    3、 Ngnix error.log出现:upstream sent too big header while reading response header from upstream错误

    1)如果是nginx反向代理
       proxy是nginx作为client转发时使用的,如果header过大,超出了默认的1k,就会引发上述的upstream sent too big header (说白了就是nginx把外部请求给后端server,后端server返回的header  太大nginx处理不过来就导致了。

      server {
            listen       80;
            server_name  *.xywy.com ;

            large_client_header_buffers 4 16k;

            location / {

              #添加这3行 
               proxy_buffer_size 64k;
               proxy_buffers   32 32k;
               proxy_busy_buffers_size 128k;

               proxy_set_header Host $host;
               proxy_set_header X-Real-IP       $remote_addr;
               proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;

        }

    }        

    2) 如果是 nginx+PHPcgi 

      错误带有 upstream: "fastcgi://127.0.0.1:9000"。就该 

      多加:

      fastcgi_buffer_size 128k;
      fastcgi_buffers 4 128k;

    server {
            listen       80;
            server_name  ddd.com;
            index index.html index.htm index.php;
       
            client_header_buffer_size 128k;
            large_client_header_buffers 4 128k;
            proxy_buffer_size 64k;
            proxy_buffers 8 64k;
            fastcgi_buffer_size 128k;
            fastcgi_buffers 4 128k;

            location / {

              ......

            }

    }         

     

    八  、 Nginx的php漏洞


    漏洞介绍:nginx是一款高性能的web服务器,使用非常广泛,其不仅经常被用作反向代理,也可以非常好的支持PHP的运行。80sec发现其中存在一个较为严重的安全问题,默认情况下可能导致服务器错误的将任何类型的文件以PHP的方式进行解析,这将导致严重的安全问题,使得恶意的攻击者可能攻陷支持php的nginx服务器。
    漏洞分析:nginx默认以cgi的方式支持php的运行,譬如在配置文件当中可以以
    location ~ .php$ {
    root html;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
    include fastcgi_params;
    }


    的方式支持对php的解析,location对请求进行选择的时候会使用URI环境变量进行选择,其中传递到后端Fastcgi的关键变量SCRIPT_FILENAME由nginx生成的$fastcgi_script_name决定,而通过分析可以看到$fastcgi_script_name是直接由URI环境变量控制的,这里就是产生问题的点。而为了较好的支持PATH_INFO的提取,在PHP的配置选项里存在cgi.fix_pathinfo选项,其目的是为了从SCRIPT_FILENAME里取出真正的脚本名。
    那么假设存在一个http://www.80sec.com/80sec.jpg,我们以如下的方式去访问

    http://www.80sec.com/80sec.jpg/80sec.php
    将会得到一个URI
    /80sec.jpg/80sec.php

    经过location指令,该请求将会交给后端的fastcgi处理,nginx为其设置环境变量SCRIPT_FILENAME,内容为
    /scripts/80sec.jpg/80sec.php
    而在其他的webserver如lighttpd当中,我们发现其中的SCRIPT_FILENAME被正确的设置为
    /scripts/80sec.jpg
    所以不存在此问题。
    后端的fastcgi在接受到该选项时,会根据fix_pathinfo配置决定是否对SCRIPT_FILENAME进行额外的处理,一般情况下如果不对fix_pathinfo进行设置将影响使用PATH_INFO进行路由选择的应用,所以该选项一般配置开启。Php通过该选项之后将查找其中真正的脚本文件名字,查找的方式也是查看文件是否存在,这个时候将分离出SCRIPT_FILENAME和PATH_INFO分别为
    /scripts/80sec.jpg和80sec.php
    最后,以/scripts/80sec.jpg作为此次请求需要执行的脚本,攻击者就可以实现让nginx以php来解析任何类型的文件了。

    POC: 访问一个nginx来支持php的站点,在一个任何资源的文件如robots.txt后面加上/80sec.php,这个时候你可以看到如下的区别:

    访问http://www.80sec.com/robots.txt

    HTTP/1.1 200 OK
    Server: nginx/0.6.32
    Date: Thu, 20 May 2010 10:05:30 GMT
    Content-Type: text/plain
    Content-Length: 18
    Last-Modified: Thu, 20 May 2010 06:26:34 GMT
    Connection: keep-alive
    Keep-Alive: timeout=20
    Accept-Ranges: bytes


    访问访问http://www.80sec.com/robots.txt/80sec.php

    HTTP/1.1 200 OK
    Server: nginx/0.6.32
    Date: Thu, 20 May 2010 10:06:49 GMT
    Content-Type: text/html
    Transfer-Encoding: chunked
    Connection: keep-alive
    Keep-Alive: timeout=20
    X-Powered-By: PHP/5.2.6


    其中的Content-Type的变化说明了后端负责解析的变化,该站点就可能存在漏洞。

    漏洞厂商:http://www.nginx.org

    解决方案:

    我们已经尝试联系官方,但是此前你可以通过以下的方式来减少损失

    关闭cgi.fix_pathinfo为0

    或者

    if ( $fastcgi_script_name ~ ..*/.*php ) {
    return 403;
    }

    PS: 鸣谢laruence大牛在分析过程中给的帮助

     

    展开全文
  • Android二维码原理优化方向

    万次阅读 多人点赞 2020-03-03 16:37:31
    } 优化相机设置 二维码扫描解码除了上述因素外,还有一个重大的相关因素就是相机设置方面的。如果我们预览的图片模糊、或者二维码拉伸、图片过小、图片旋转或者扭曲等,都会导致很难定位到二维码,解析二维码困难。...

    困惑

    做过Android的二维码扫描的童鞋可能会遇到过,很多二维码识别不了,或者识别速度慢。一般造成这个识别不出来的原因,大概以下几点:

    • Android手机配置不一样,手机像素高低也有不同,有的手机还不支持自动对焦
    • 环境影响也特别大,亮光,反光、粉尘、灰尘、距离都会导致识别不佳
    • A4纸张打印的标签二维码,本来打印就不是特别清晰,有些像素点,不一定都打印了出来

    但是用微信扫一扫,却能很快的识别出上面几种情况造成的二维码;又或者用支付宝或者钉钉二维码扫描,一样也能识别出来;还有iOS也就是调用系统的扫描,也一样能够扫描出来,为啥我们自己的不行?老板不管这些,只是说了,别人的可以,为啥你的不可以,那就是你的问题…

    于是网上找了很多各种几千个赞的第三方集成的二维码,发现也不怎么理想,总是比不上微信、支付宝等。Github上何种上千Star的第三方库都是基于ZXing或者ZBar,最后一圈下来你得出结论:ZXing和ZBar不行。你会想:这微信和支付宝都是基于啥开发的,如果能开源一下那就太好了。下面我们就聊一聊微信扫一扫与支付宝扫一扫的原理~

    微信扫一扫

    微信官方公众号“微信派”就特别介绍了微信二维码扫描功能的一些技术细节。

    预判算法

    微信扫码使用了自家开发的QBar引擎,并计入了预判算法,在识别条码之前会过滤无码图像,只识别有意义的内容(二维码和条形码)。

    整个扫码预判模块位于核心识别引擎之前,不再需要对输入的视频中的每一帧图像进行检测识别,能实现快速过滤大量无码图像,减少后续不必要的定位和识别对扫码客户端造成的阻塞,使响应更加及时,增加扫码过程中的流畅度,而这就是微信扫码快速的关键原因。

    微信团队分析数据显示,该引擎在识别正常图片时的解码速度,iOS可缩短至5毫秒,安卓也仅仅约12毫秒,当然这也和手机配置尤其是摄像头有很大关系。

    容错性解码、多语言字符检测算法

    QBar扫码引擎对二维码容错性解码算法、多语言字符检测算法等均使用进行了数十项优化,在识别率和识别速度上得到了提升。

    一些二维码就算出现穿孔、污损或者弯折,还是一样可以识读,这是因为二维码中存储的信息通常都经过了纠错编码,是有冗余的。

    一个二维码所能表示的比特数是固定的,包含的信息越多那么冗余度就越小,反之亦然。微信二维码中包含的信息量并不需要很大,这意味着编码的冗余度可以做得较高,所以即使损毁面积达到30%也依旧可以恢复。简单来说,QBar识别及解码的流程包括:

    • 读取视频或图像,通过灰度化处理得到单张灰度图;
    • 对灰度图进行处理得到二值图像(二值化是引擎在识别前把图像转换成01图像的过程);
    • 将二值图输入不同的解码器识别是否存在二维码;
    • 如果检测到存在某种编码,即通过相应的解码器进行解码,并返回解码结果。

    在提高成功解码的概率上,微信的另一个做法就是给每个步骤做上“标签”,目的是找出错误信息,对失败信息再设置相应的二次检测流程,在失败的步骤处进行更”努力”地尝试,且越到后面的步骤,表明图中存在二维码的概率越大。

    通过这样每步找错、多次识别解码,大大提高了手机扫描二维码的成功率。

    什么是QBar

    上面说的QBar好像很牛逼,微信又不开源,说的越牛逼越觉得坑爹:你这么牛逼我又用不了。

    我们尝试着去窥探微信安装包,打开目录lib\armeabi。里面包涵了微信安卓应用所用到的C/C++动态链接库。大概扫一下,看到了libwechatQrMod.so,应该就是用于二维码的了。用atom打开,居然发现了这个:

    在这里插入图片描述
    微信的1D/2D barcode解码居然用的是开源的ZXing!突然对ZXing燃起了希望。没错,QBar的底层就是ZXing,不过微信团队做了非常多的优化。当然了,我们也是可以优化的,下一步可以好好研究优化方向了~~

    支付宝扫一扫

    支付宝扫一扫是基于libqrencode 库集成的,既然ZXing可以做到这个效果,libqrencode 就没必要再过多研究。

    ZXing扫码优化

    一般我们做二维码扫描的功能,会到https://github.com/zxing/zxing 拉代码,然后取出Android部分的demo运行,成功之后便开始移植到自己的工程。如果是这样,那么恭喜,你已经入坑了。官方的demo,扫码功能无可厚非是没问题的,但是因为是一个大而全的demo,更多考虑的是功能的集成。实际上我们每个项目的需求不同,当你的需求考虑上扫码速度与识别率的时候,官方的demo就会显得有点跟不上。你会发现:

    微信扫一扫是基于ZXing,你的扫一扫也是基于ZXing,为啥识别速度与识别效率天差地别?

    所以我们需要做大量的定制型优化

    减少解码格式提高解码速度

    ZXing默认支持15种格式,支持格式有QR Code、Aztec、Code 128、Code 39、EAN-8 等等。然后我们在实际中用不到这么多解码样式,我们常见的二维码格式是QR Code,一维码格式为Code 128, 如果无特殊要求,这两种格式就能满足一般的条码与二维码的需求。 在解码过程中减少一种解码,就会减少解析时间,提高解码速度。所以我们在实践过程中可以根据实际减少解码样式,提高解码速度,如果app实际只有二维码扫码,甚至可以只保留QR Code这一种解码格式。

    ZXing 我们可以修改DecodeFormatManager 及DecodeThread这两个类减少解码种类

    //DecodeFormatManager.java 只保留二维码相关
    static {
    	...
        QR_CODE_FORMATS = new Vector<BarcodeFormat>(1);
        QR_CODE_FORMATS.add(BarcodeFormat.QR_CODE);
        ...
    }
    //DecodeThread.java 只保留二维码相关
    if (decodeFormats == null || decodeFormats.isEmpty()) {
        decodeFormats = new Vector<BarcodeFormat>();
        decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
    }
    

    解码算法优化

    目前我们在Zxing我们能看到HybridBinarizer及GlobalHistogramBinarizer,HybridBinarizer继承自GlobalHistogramBinarizer,在其基础上做了功能改进。这两个类都是Binarizer的实现类,都是基于二值化,将图片的色域变成黑白两个颜色,然后提取图形中的二维码矩阵。

    官网上介绍GlobalHistogramBinarizer算法适合低端设备,对手机CPU和内存要求不高。但它选择了全部的黑点来计算,因此无法处理阴影和渐变这两种情况。HybridBinarizer的算法在执行效率上要慢于GlobalHistogramBinarizer算法,但识别相对更加有效,它专门以白色为背景的连续黑块二维码图像解析而设计,也更适合来解析更具有严重阴影和渐变的二维码图像。

    zxing项目官方默认使用的是HybridBinarizer二值化方法。然而目前的大部分二维码都是黑色二维码,白色背景的。不管是二维码扫描还是二维码图像识别,使用GlobalHistogramBinarizer算法的效果要稍微比HybridBinarizer好一些,识别的速度更快,对低分辨的图像识别精度更高。可以在DecodeHandler 中更改算法:

    private void decode(byte[] data, int width, int height) {
        //优先GlobalHistogramBinarizer解码,解码失败转为HybridBinarizer解码
        BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
        if(bitmap  == null){
        	bitmap = new BinaryBitmap(new HybridBinarizer(source));
        }
    }
    

    便提下,微信扫码使用了自家开发基于ZXing的QBar引擎,并导入了预判算法,在识别条码之前会过滤无码图像,只识别有意义的内容——二维码和条形码。整个扫码预判模块位于核心识别引擎之前,不再需要对输入的视频中的每一帧图像进行检测识别,能实现快速过滤大量无码图像,减少后续不必要的定位和识别对扫码客户端造成的阻塞,使响应更加及时,增加扫码过程中的流畅度,而这就是微信扫码快速的关键原因。

    减少解码数据

    现在的手机拍照的照片像素都很高,目前市场上好一点手机像素都上千万,拍摄一张照片的就十几M, 这个大的数据量对解码很有压力,我们在开发过程有必要采取措施减少解码数据量。

    官方为了减少解码的数据,提高解码效率和速度,利用扫码区域范围来裁剪裁剪无用区域,减少解码数据。我们在开发过程可以调整好扫码区域,减少解码的数据量。

    private void decode(byte[] data, int width, int height) {
       //只识别的识别框的区域
    	scanBoxAreaRect = mScanBoxView.getScanBoxAreaRect(height);
    	PlanarYUVLuminanceSource  = new PlanarYUVLuminanceSource(
    		data, 
    		width, 
    		height, 
    		scanBoxAreaRect.left, 
    		scanBoxAreaRect.top, 
    		scanBoxAreaRect.width(), 
    		scanBoxAreaRect.height(), 
    		false
    	);
    }
    

    将处理相机帧从串行改为并行

    在这里插入图片描述
    ZXing的demo每次从onPreviewFrame()中获取一帧数据,发送R.id.decode的handler消息队列,然后调用zxing的decode解析二维码,如果成功,则返回;如果失败,则调用setOneShotPreviewCallback( ),重新调用一次onPreviewFrame( )。

    缺点是如果处理一帧数据时间很长,会阻碍下一帧的处理,比如上一帧是没有二维码的,而下一帧是有二维码的,如果上一帧处理时间较长,那么虽然用户对准了二维码,但是实际处理的还是上一帧,因此不太合理。

    //DecodeHandler.java
    @Override
    public void handleMessage(Message message) {
        if (message.what == R.id.decode) {
            decode((byte[]) message.obj, message.arg1, message.arg2);
        }
    }
    

    我们将串行处理改成并行处理,一旦从onPreviewFrame( )获取一帧数据,将decode任务丢进线程池,并立即调用setOneShotPreviewCallback( )获取下一帧数据。一旦某个任务检测到二维码,立即将isSuccess变量置为true,忽略其他任务。这样能够大大加快二维码检测的速度。

    @Override
    public void onPreviewFrame(final byte[] data, final Camera camera) {
        ...
        mProcessDataTask = new ProcessDataTask(camera, data, this,HDQRCodeUtil.isPortrait(getContext())).perform();
    }
    

    优化相机设置

    二维码扫描解码除了上述因素外,还有一个重大的相关因素就是相机设置方面的。如果我们预览的图片模糊、或者二维码拉伸、图片过小、图片旋转或者扭曲等,都会导致很难定位到二维码,解析二维码困难。

    选择最佳预览尺寸/图片尺寸

    如果手机摄像头生成的预览图片宽高比和手机屏幕像素宽高比(准确地说是和相机预览屏幕宽高比)不一样的话,投影的结果肯定就是图片被拉伸。现在基本上每个摄像头支持好几种不同的预览尺寸(parameters.getSupportedPreviewSizes()),我们可以根据屏幕尺寸来选择相机最适合的预览尺寸,当然如果相机支持的预览尺寸与屏幕尺寸一样更好,否则就找到宽高比相同,尺寸最为接近的。

    //一下算法是:比例优先 尺寸接近次之
    Camera.Size  mCameraResolution = findCloselySize(displayMetrics.widthPixels, displayMetrics.heightPixels,parameters.getSupportedPreviewSizes());
    Camera.Parameters parameters = camera.getParameters();
    parameters.setPreviewSize(mCameraResolution.width, mCameraResolution.height);
    camera.setParameters(parameters );
    
    /**
     * 通过对比得到与宽高比最接近的尺寸(如果有相同尺寸,优先选择)
     *
     * @param surfaceWidth  需要被进行对比的原宽
     * @param surfaceHeight 需要被进行对比的原高
     * @param preSizeList   需要对比的预览尺寸列表
     * @return 得到与原宽高比例最接近的尺寸
     */
    protected Camera.Size findCloselySize(int surfaceWidth, int surfaceHeight, List<Camera.Size> preSizeList) {
        Collections.sort(preSizeList, new SizeComparator(surfaceWidth, surfaceHeight));
        return preSizeList.get(0);
    }
    
    /**
     * 预览尺寸与给定的宽高尺寸比较器。首先比较宽高的比例,在宽高比相同的情况下,根据宽和高的最小差进行比较。
     */
    private static class SizeComparator implements Comparator<Camera.Size> {
        private final int width;
        private final int height;
        private final float ratio;
        SizeComparator(int width, int height) {
        	//不管横屏还是竖屏,parameters.getSupportedPreviewSizes()的size.width 始终大于 size.height
            if (width < height) {
                this.width = height;
                this.height = width;
            } else {
                this.width = width;
                this.height = height;
            }
            this.ratio = (float) this.height / this.width;
        }
    
        @Override
        public int compare(Camera.Size size1, Camera.Size size2) {
            int width1 = size1.width;
            int height1 = size1.height;
            int width2 = size2.width;
            int height2 = size2.height;
            float ratio1 = Math.abs((float) height1 / width1 - ratio);
            float ratio2 = Math.abs((float) height2 / width2 - ratio);
            int result = Float.compare(ratio1, ratio2);
            if (result != 0) {
                return result;
            } else {
                int minGap1 = Math.abs(width - width1) + Math.abs(height - height1);
                int minGap2 = Math.abs(width - width2) + Math.abs(height - height2);
                return minGap1 - minGap2;
            }
        }
    }
    

    还有另外一种算法:尺寸最接近优先

    private static Point findBestPreviewSizeValue(List<Camera.Size> supportSizeList, Point screenResolution) {
        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        for (Camera.Size previewSize : supportSizeList) {
            int newX = previewSize.width;
            int newY = previewSize.height;
            int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
            if (newDiff == 0) {
                bestX = newX;
                bestY = newY;
                break;
            } else if (newDiff < diff) {
                bestX = newX;
                bestY = newY;
                diff = newDiff;
            }
        }
        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }
        return null;
    }
    

    设置适合的放大倍数

    当我们对准二维码时候发现,相机离二维码比较远时,预览的二维码比较小;当相机靠近时,预览的二维码比较大。当我们的二维码过小时,发现条码很难扫出来。另外测试发现每个手机的放大倍数不是都是相同的,这可能与各个手机的信号相关。如果直接设置为一个固定值,这可能会在某些手机上过度放大,某些手机上放大的倍数不够。索性相机的参数设定里给我们提供了最大的放大倍数值,通过取放大倍数值的N分之一作为当前的放大倍数,就完美地解决了手机的适配问题。

    private void setZoom(Camera.Parameters parameters) {
    
        String zoomSupportedString = parameters.get("zoom-supported");
        if (zoomSupportedString != null && !Boolean.parseBoolean(zoomSupportedString)) {
            return;
        }
    
        int tenDesiredZoom = 27;
    
        String maxZoomString = parameters.get("max-zoom");
        if (maxZoomString != null) {
            try {
                int tenMaxZoom = (int) (10.0 * Double.parseDouble(maxZoomString));
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad max-zoom: " + maxZoomString);
            }
        }
    
        String takingPictureZoomMaxString = parameters.get("taking-picture-zoom-max");
        if (takingPictureZoomMaxString != null) {
            try {
                int tenMaxZoom = Integer.parseInt(takingPictureZoomMaxString);
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad taking-picture-zoom-max: " + takingPictureZoomMaxString);
            }
        }
    
        String motZoomValuesString = parameters.get("mot-zoom-values");
        if (motZoomValuesString != null) {
            tenDesiredZoom = findBestMotZoomValue(motZoomValuesString, tenDesiredZoom);
        }
    
        String motZoomStepString = parameters.get("mot-zoom-step");
        if (motZoomStepString != null) {
            try {
                double motZoomStep = Double.parseDouble(motZoomStepString.trim());
                int tenZoomStep = (int) (10.0 * motZoomStep);
                if (tenZoomStep > 1) {
                    tenDesiredZoom -= tenDesiredZoom % tenZoomStep;
                }
            } catch (NumberFormatException nfe) {
                // continue
            }
        }
    
        // Set zoom. This helps encourage the user to pull back.
        // Some devices like the Behold have a zoom parameter
        if (maxZoomString != null || motZoomValuesString != null) {
            parameters.set("zoom", String.valueOf(tenDesiredZoom / 10.0));
        }
    
        // Most devices, like the Hero, appear to expose this zoom parameter.
        // It takes on values like "27" which appears to mean 2.7x zoom
        if (takingPictureZoomMaxString != null) {
            parameters.set("taking-picture-zoom", tenDesiredZoom);
        }
    }
    

    除了设置一定比例的放大倍数之外,还有一种辅助的做法是根据二维码的大小自动拉近摄像头。微信就是这么干的:当发现二维码距离比较远的时候自动拉近摄像头,也就是加大放大倍数。建议的做大:二维码在扫描框中的宽度小于扫描框的 1/4,放大最大倍数的 1/4 镜头:

    // 二维码在扫描框中的宽度小于扫描框的 1/4,放大镜头
    final int maxZoom = parameters.getMaxZoom();
    final int zoomStep = maxZoom / 4;
    final int zoom = parameters.getZoom();
    post(new Runnable() {
        @Override
        public void run() {
            startAutoZoom(zoom, Math.min(zoom + zoomStep, maxZoom), result);
        }
    });
    

    合理的对焦策略

    ZXing 默认的聚焦间隔时间是2000毫秒。扫码是在每一次调用相机聚焦完成后触发回调取图解析的。在这里缩短聚焦时间会提高解析频率,扫码性能自然就提升了。这里建议采用连续对焦模式提升解析频率。

    /**
     * 连续对焦
     */
    private void startContinuousAutoFocus() {
        try {
            Camera.Parameters parameters = mCamera.getParameters();
            // 连续对焦
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            mCamera.setParameters(parameters);
            // 要实现连续的自动对焦,这一句必须加上
            mCamera.cancelAutoFocus();
        } catch (Exception e) {
            HDQRCodeUtil.e("连续对焦失败");
        }
    }
    

    除了对焦模式,对焦策略也是非常重要。要辅助选择触摸区域对焦,双指缩放对焦倍数,Android 4.0 以后设置合适的对焦区域和测光区域来优化识别效率。

    private void handleFocusMetering(float originFocusCenterX, float originFocusCenterY,  int originFocusWidth, int originFocusHeight) {
        try {
            boolean isNeedUpdate = false;
            Camera.Parameters focusMeteringParameters = mCamera.getParameters();
            Camera.Size size = focusMeteringParameters.getPreviewSize();
            if (focusMeteringParameters.getMaxNumFocusAreas() > 0) {//支持设置对焦区域
                isNeedUpdate = true;
                Rect focusRect = HDQRCodeUtil.calculateFocusMeteringArea(1f,
                        originFocusCenterX, originFocusCenterY,
                        originFocusWidth, originFocusHeight,
                        size.width, size.height);
                focusMeteringParameters.setFocusAreas(Collections.singletonList(new Camera.Area(focusRect, 1000)));
                focusMeteringParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
            }
            if (focusMeteringParameters.getMaxNumMeteringAreas() > 0) {//支持设置测光区域
                isNeedUpdate = true;
                Rect meteringRect = HDQRCodeUtil.calculateFocusMeteringArea(1.5f,
                        originFocusCenterX, originFocusCenterY,
                        originFocusWidth, originFocusHeight,
                        size.width, size.height);
                focusMeteringParameters.setMeteringAreas(Collections.singletonList(new Camera.Area(meteringRect, 1000)));
            }
            if (isNeedUpdate) {
                mCamera.cancelAutoFocus();
                mCamera.setParameters(focusMeteringParameters);
                mCamera.autoFocus(new Camera.AutoFocusCallback() {
                    public void onAutoFocus(boolean success, Camera camera) {
                        //对焦结果
                    }
                });
            }
        } catch (Exception e) {//对焦测光失败
            e.printStackTrace();
        }
    }
    

    加大二维码的颜色对比度

    二维码识别,如下图,常规二维码为了方便识别选择了两个对比度最大的颜色-黑色与白色,在重新设计二维码的时候要注意二维码颜色和背景颜色保持一定的深浅对比度,注意二维码不能使用白色,白色代表编码 0,黑色代表编码 1,反白之后编码会错误,二维码将不能识别。
    在这里插入图片描述

    二维码原理

    接下来讲讲二维码的具体原理,为什么放在后面讲?因为:

    二维码原理太复杂了,如果放在前面,我相信你看了几分钟就不想看了。即使你坚持看完了,也是一脸懵逼,想自己实现一下基本是不可能。倒不如先讲完优化部分再讲原理,实操第一嘛~

    首先我们要了解一下二维码是什么?二维码又称二维条码,常见的二维码为 QR Code,QR 全称 Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型。如下图:传统条形码在 X 轴上存储信息,二维码则多加了 Y 轴。
    在这里插入图片描述
    二维码存储数据的方式采用二进制语言,对于电脑程序来说,有 0 和 1 这两个数字就有了一切!在二维码中,白色的方块代表 0,黑色的方块代表 1。
    在这里插入图片描述
    二维码存在 40 种尺寸,在官方文档中,尺寸又被命名为 Version,这个version下面会经常提到,先努力记下来。尺寸与 Version 存在线性关系:Version 1 是 21×21 的矩阵,Version 2 是 25×25 的矩阵,每增加一个 Version,尺寸都会增加 4,故尺寸 Size 与 Version 的线性关系为:

    在这里插入图片描述
    Version 的最大值是 40,故尺寸最大值是(40-1)*4+21 = 177,即 177 x 177 的矩阵。

    二维码结构图

    在这里插入图片描述

    • 空白区
      固定不变,用来快速区分周围环境与二维码。
    • 位置探测图形
      固定不变,用于标记二维码矩形的大小;用三个定位图案即可标识并确定一个二维码矩形的位置和方向了。
    • 位置探测图形分隔符
      固定不变,用白边框将定位图案与其他区域区分。
    • 定位图形
      固定不变,用于定位,二维码如果尺寸过大,扫描时容易畸变,定位图形的作用就是防止扫描时畸变的产生。
    • 校正图形
      由三个黑白相间的小正方形嵌套组成一个大的正方形,仅在版本Version>=2的情况下存在(version=1没有),而且不同版本的个数不一样。作用是便于确定中心,纠正扭曲。
    • 格式信息
      形状位置固定,内容变化,用来记录使用的掩码纠错等级
    • 板信息
      形状位置固定,内容变化,仅在版本Version>=7的情况下存在,需要预留两块 3×6 的区域记录具体的版本信息,版本6以及以下全为0。
    • 数据和纠错码字
      剩下的区域,用来保存二维码信息和纠错码字(用于修正二维码损坏带来的错误)。

    位置探测图形

    定位图案与尺寸大小无关,一定是一个 7×7 的矩阵。
    在这里插入图片描述

    定位图形

    对齐图案与尺寸大小无关,一定是一个 5×5 的矩阵。
    在这里插入图片描述
    定位图形的个数和位置规则按下表摆放(只列举version<=25部分)。其中第一列对应Version版本号,第二列表示最终得到的定位图的个数,第三列表示所列举的数字进行两两组合(包含自身)形成的坐标点就是定位图标的中心坐标点不包括已经定义好的功能的位置点。

    在这里插入图片描述
    下面针对上述表格中 Version 8 的一个例子。对于 Version 8 的二维码,行列值在 6, 24, 42 两辆组合得到坐标:(6,6)、(6,24)、(6,42)、(24,6)、(24,24)、(24,42)、(42,6)、(42,24)、(42,42)

    在这里插入图片描述
    本来存在3*3=9种情况,但是黄色部分的坐标与位置探测图形的坐标重合,不算,剩下6个,如上图所示。

    校正图形

    校正图形,是两条黑白相间的连接三个定位图案的线。如下图所示。
    在这里插入图片描述

    格式信息

    格式信息在定位图案周围分布,由于定位图案个数固定为 3 个,且大小固定,故格式信息也是一个固定 15bits 的信息。
    在这里插入图片描述
    每个 bit 的位置如下图(注:图中的 Dark Module 是固定不变的)。

    在这里插入图片描述
    15bits 中数据,按照 5bits 的数据位 + 10bits 纠错位的顺序排列。但是,最终的15bits数据并不是简单的数据位+接错位,为了减少扫描后图像识别的困难,最后还需要将 15bits 的数据 与 101010000010010 做异或 XOR 操作。因为我们在原格式信息中可能存在太多的 0 值(如纠错级别为 00,蒙版 Mask 为 000),使得格式信息全部为白色,这将增加分析图像的困难。

    5bits 的数据位

    数据位占 5bits:其中 2bits 用于表示使用的纠错等级 (Error Correction Level),3bits 用于表示使用的蒙版 (Mask) 类别。

    • 纠错等级
      二维码被遮挡部分或者加入logo也能识别出来,原因就是纠错机制。二维码存在4个级别的纠错等级,纠错级别越高,可以修正的错误就越多,需要的纠错码的数量也变多,相应的可储存的数据就会减少。
    纠错等级 编码 纠错水平
    L 01 7%字码修正
    M 00 15%字码修正
    Q 11 25%字码修正
    H 10 30%字码修正
    • 蒙版 (Mask)
      如果出现大面积的空白或黑块,会造成我们扫描识别的困难。所以,我们还要做Masking操作,QR有8个Mask你可以使用,Mask对应的编码以及算法如下所示。其中,各个mask的公式在各个图下面。所谓mask,就是和上面生成的图做XOR操作。Mask只会和数据区进行XOR,不会影响功能区。
      在这里插入图片描述

    下面是原来比较块状的二维码经过Mask后的一些样子,我们可以看到被某些Mask XOR了的数据变得比较零散了。

    在这里插入图片描述

    10bits 纠错位

    上述5bits 的数据位进行 BCH Code算法介绍) 计算之后生成的10bits纠错码,具体怎么计算这里就不提了。

    最后举例子:假设存在纠错等级为 M(对应 00),蒙版图案对应 101,5bits 的数据位为 00101,10bits 的纠错位为 0011011100。生成了在异或操作之前的 bits 序列为:001010011011100 。与 101010000010010 做异或 XOR 操作,即得到最终格式信息:100000011001110

    版本信息

    对于 Version 7 及其以上的二维码,需要加入版本信息,如下面蓝色区域。
    在这里插入图片描述
    版本信息依附在定位图案周围,故大小固定为 18bits。水平竖直方向的填充方式j以及填充顺序如下图所示。如果再版本6以及以下,这个区域全为0。18bits 的版本信息中,前 6bits 为版本号 (Version Number),后 12bits 为纠错码 (BCH Bits)。示例如下:

    假设存在一个 Version 为 7 的二维码(对应 6bits 版本号为 000111),其纠错码为 110010010100,则版本信息图案中的应填充的数据为:000111110010010100
    在这里插入图片描述

    数据码和纠错码

    填充数据码和纠错码的思想如下图二维码所示( 以Version 3实例 ),从二维码的右下角开始,沿着红线进行填充,遇到非数据区域,则绕开或跳过

    在这里插入图片描述
    然而这样难以理解,我们可以将其分为许多小模块,每八个方格组成一个小块,然后将许多小模块串连在一起,如下图所示。灰色的D区域表示的是数据区存放的区域,白色的E区域表示的是纠错码数据存放区域。最后还有部分空白的剩余位,如下图的Remainder Bits。

    在这里插入图片描述
    小模块可以分为常规模块和非常规模块,每个模块的容量都为 8。常规情况下,小模块都为宽度为 2 的竖直小矩阵,按照方向将 8bits 的码字填充在内。非常规情况下,模块会产生变形。
    填充方式上图 6.14,图中深色区域(如 D1 区域)填充数据码,白色区域(如 E15 区域)填充纠错码。遍历顺序依旧从最右下角的 D1 区域开始,按照蛇形方向(D1→D2→…→D28→E1→E2→…→E16→剩余码)进行小模块的填充,并从右向左交替着上下移动。其中每个小模块的填充规则稍微有些繁琐复杂,因为模块形状各种各样都有。下面举个规则模块的填充顺序,其他的这里不做介绍(下图分别对应D1、D8两块):

    在这里插入图片描述

    那么,数据区和纠错码的数据是怎么个生成规则呢?

    针对不同的数据,QR码设计了不同的数据编码编码方式,我们可以根据数据的种类选择合适的编码方式进行编码。通过编码之后的数据码经过一定的规则生成纠错码,就组成了我们的数据码和纠错码区域的数据。下面我们详细展开说明。

    数据码

    数据编码的过程将数据字符转换为二进制位流,每8位一个码字,整体构成一个数据的码字序列的过程。

    但是数据的格式多种多样,可以是数字、字符、汉字、日语等,要以什么样的编码模式转成二进制位流呢?先看看二维码支持的数据编码模式以及对应的指示符:

    编码模式 指示符
    ECI编码 0111
    数字编码 0001
    字符编码 0010
    8位字节编码 0100
    日本汉字编码 1000
    中文汉字编码 1101
    结构链接编码 0011
    FNCI编码 0101(第一位置)1001(第二位置)
    终止符(信息结尾) 0000

    对于特定的编码模式,并不是说单个数据占的二进制位数就确定了,如果二维码的Version版本不一样,单个数据所占的二进制位数也会有不同。下面我们看看不同Version下编码和位数规则

    二维码Version 数字编码 字符编码 8位字节编码 汉字编码
    1 to 9 10 9 8 8
    10 to 26 12 11 16 10
    27 to 40 14 13 16 12

    下面我们看看常用的几种数据编码:

    • 数字编码

    数字编码的范围为 0~9。 对于数字编码,统计需要编码数字的个数是否为 3 的倍数。如果不是 3 的倍数,则剩下的 1 位转成 4bits 、2 位会被转为 7bits,否则每三位数字都会转为 10bits 的二进制结果。分组之后对应的是十进制,最后转成对应的二进制流。最后对数字的个数同样编成上面提到的 10、12、14 bits(参照不同Version下编码和位数规则),再加上头部编码指示符和尾部结束符0000形成最终的编码。

    举例子:Version = 1 数据为:01234
    1、可分为两组:012和34 分别对应10位二进制7位二进制的十进制12和34
    2、数字编码为:012→0000001100 + 34→0100010 : 0000001100 0100010
    3、加上数字个数5对应 10bits 编码为(0000000101):0000000101 0000001100 0100010
    4、加上头部编码指示符:0001 0000000101 0100010 0000001100 0100010
    5、加上结尾结束符:0001 0000000101 0100010 0000001100 0100010 0000
    6、最终编码:01234 → 0001 0000000101 0100010 0000001100 0100010 0000

    • 字符编码

    包括 0-9,大写的A到Z(没有小写),以及符号$ % * + – . / : 和空格。这些字符会映射成一个字符索引表。字符编码的过程,就是将每两个字符分为一组,然后转成下图的45 进制,再转为 11bits 的二进制结果。对于落单的一个字符,则转为 6bits 的二进制结果。 然后字符个数转成9、11、13 bits 二进制的二进制(参照不同Version下编码和位数规则),再加上头部编码指示符和尾部结束符0000形成最终的编码。

    在这里插入图片描述
    其中的SP是空格,Char是字符,Value是其索引值。

    举例子:Version = 1 数据为:AE6
    1、可分为两组:AE和6 分别(10, 14) 、 (6)
    2、字符编码应将字符组转换为 11bits 的二进制:

    • (10, 14):转为 45 进制:10×45+14 = 464;再转为 11bits 的二进制:00111010000
    • (6):转为 45 进制:6;再转为 6bits 的二进制:000110

    3、加上数字个数3对应 9bits 编码(000000011): 000000011 00111010000 000110
    4、加上头部编码指示符:0010 000000011 00111010000 000110
    5、加上结尾结束符:0010 000000011 00111010000 000110 0000
    6、最终编码为:AE6 → 0010 000000011 00111010000 000110 0000

    • 8位字节编码

    可以是 0-255 的 ISO-8859-1 字符。有些二维码的扫描器可以自动检测是否是 UTF-8 的编码。

    • 其他编码

    对于其他编码,用得不多,这里就不展开讲解了。

    上面提到的。对于数字01234最终编码为 0001 0000000101 0100010 0000001100 0100010 0000或者字符AE6的编码0010 000000011 00111010000 000110 0000是不是就是最终二维码填充的数据码了?其实不是的,我们最终的数据是以8个为一个数据单元填充,如果编码个数不为8的倍数需要补0;

    数据 编码指示符 字符数编码 数据编码 结束符 不足8位补0
    01234 0001 0000 000101 01 00010000 00011000 100010 00 00 000000
    AE6 0010 0000 00011 001 11010000 000110 00 00 000000

    加了8倍数补位0之后还没完,如果最后还没有达到我们最大的 Bits 数限制,则需要在编码最后加上补齐符(Padding Bytes)。
    补齐符内容是不停重复两个字节:11101100 和 00010001。这两个二进制转成十进制,分别为 236 与17。关于每一个Version的每一种纠错级别的最大Bits限制可以参考下图:
    在这里插入图片描述
    上图中提到的 codewords,可译为码字,一个码字是一个字节,一个字节 8bits。对于 Version,共需要 26 个码字,即 208bits。对于上述部分已经部分编码的数字01234和字符AE6:

    数据 不足8倍补位0之后的编码 已用位数 还差位数 补齐符
    01234 00010000 00101010 00100000 0011000 10001000 00000000 48 160 11101100 00010001 x 20
    AE6 00100000 00011001 11010000 00011000 00000000 40 168 11101100 00010001 x 21

    最终的数据码(Data Codewords):用于最终填充在二维码D区域的数据

    数据 数据码(Data Codewords)
    01234 00010000 00101010 00100000 0011000 10001000 00000000 11101100 00010001 … 00010001 11101100
    AE6 00100000 00011001 11010000 00011000 00000000 11101100 00010001 … 11101100 00010001

    纠错码

    根据前面的分析,我们已经生成了数据码,填充D区域,但是E区域的纠错码又是什么规则生成的呢?纠错码是在数据码的基础上生成的,首先要对数据码进行分组,即分成不同的块(Block)。分组规则参考下图:
    加粗样式在这里插入图片描述
    主要看两个概念:

    • 纠错块个数(Number of error correction blocks):需要划分纠错快的个数
      参照表格,大部分是一个数字,比如1表示不同分组,2表示分成两组;还有一种(2 2)的,表示分成两组,每一组分成两块。
    • 纠错块码字数(Error Correction Code Per Blocks):每个块中的码字个数,即有多少个字节Bytes
      表中最下面关于 (c,k,r) 的解释:
      c:码字总个数;
      k:数据码个数;
      r:纠错码容量
      c,k,r的关系公式:c = k+2×r。但是对于标号b区的数据,存在c > k+2×r,属于特殊情况。

    纠错码的生成:

    纠错码主要是通过里德-所罗门纠错算法(Reed-Solomon Error Correction)实现,这里就不展开讲了

    对于只有一个分组:

    单个分组采用顺序放置的方式

    紧接着上面的两个数据01234和AE6,假如他们的纠错等级都为L,那么他们的纠错码个数都为7,数据码格式为19,因为只有一个分组,所以只需要将这19+7=26个新数据按着顺序放在二维码中即可。

    当存在多个分组的情况下:

    比如Version 5 + H 纠错等级,参照表格包含着两行两列的四个块,最终的数据将采用穿插放置的规则。

    具体示例如下表所示,且由于使用二进制会使得表格过大,故转为范围在 0~255 的十进制。其中组 1 的每个块,都有 11 个数据码, 22 个纠错码;组 2 的每个块,都有 12 个数据码,22 个纠错码。

    组号 块号 数据 每个块的纠错码
    1 1-1 67 85 70 134 87 38 85 194 119 50 6 199 11 45 115 247 241 223 229 248 154 117 236 38 6 50 17 7 236 213 87 148 235
    1 1-2 66 7 118 134 242 7 38 86 22 198 199 177 212 76 133 75 242 238 76 195 230 189 106 248 134 76 40 154 27 195 255 117 129
    2 2-1 247 119 50 7 118 134 87 38 82 6 134 151 96 60 202 182 124 157 200 134 27 129 209 182 70 85 246 230 247 70 66 247 118 134
    2 2-2 194 6 151 50 16 236 17 236 17 236 17 236 173 24 147 59 33 106 40 255 172 82 2 157 242 33 229 200 238 106 248 134 76 40

    提取每一列数据:

    第一列:67, 66, 247, 194;
    第二列:85, 7, 119, 6;
    ……
    第十一列:6, 199, 134, 17;
    第十二列:151, 236;
    将上述十二列的数据拼在一起:67, 66, 247, 194, 85, 7, 119, 6,…, 6, 199, 134, 17, 151, 236。
    最终的数据码:67, 66, 247, 194, 85, 7, 119, 6,…, 6, 199, 134, 17, 151, 236

    同样的方法,将 22 列纠错码放在一起:199, 177, 96, 173, 11, 212, 60, 24, …, 148, 117, 118, 76, 235, 129, 134, 40。
    最终的纠错码:199, 177, 96, 173, 11, 212, 60, 24, …, 148, 117, 118, 76, 235, 129, 134, 40

    最后将数据码放在D区域,纠错码放在E区域,生成最终的二维码了。

    二维码绘制过程

    二维码的原理讲完了,不知道看完理解多少。就我个人而言,即使可以看懂,但是想自己实现,那太复杂了,要考虑的维度太多,各种情况都要兼容进来的话难度可想而知。下面最后看看二维码的绘制流程吧。

    • 首先在二维码的三个角上绘制位置探测图形。定位图案与尺寸大小无关,一定是一个 7×7 的矩阵。

    在这里插入图片描述

    • 然后绘制定位图形。定位图形与尺寸大小无关,一定是一个 5×5 的矩阵。

    在这里插入图片描述

    • 接着绘制校正图形:两条颜色相间连接三个定位图案的线。

    在这里插入图片描述

    • 接着绘制格式信息图形:格式信息在定位图案周围分布,由于定位图案个数固定为 3 个,且大小固定。

    在这里插入图片描述

    • 接着绘制版本信息图形:依附在定位图案周围,故大小固定为 18bits。

    在这里插入图片描述

    • 接着填充数据码与纠错码:将数据码和操作码的二进制流按着从右下角到左下角的顺序依次填充,1表示填充,0表示空白。

    在这里插入图片描述

    • 最后蒙版操作:如果出现了大面积的空白或黑块,扫描识别会十分困难,所以最后要对整个图像与蒙版进行蒙版操作(Masking),蒙版操作即为异或 XOR 操作。
      在这里插入图片描述
      当讲完前面的原理之后,回头看绘制的过程反而显得没那么难懂了,会有一种豁然开朗的感觉。但是如果还是不懂的话可以多看一遍,笔者相信这篇文章已经讲得非常清楚透彻了~
    展开全文
  • 一、启动原理解析 Android是基于Linux内核的,当手机启动,加载完Linux内核后,会由Linux系统的init祖先进程fork出Zygote进程,所有的Android应用程序进程以及系统服务进程都是这个Zygote的子进程(由它fork出来的)...

    一、启动原理解析

    Android是基于Linux内核的,当手机启动,加载完Linux内核后,会由Linux系统的init祖先进程fork出Zygote进程,所有的Android应用程序进程以及系统服务进程都是这个Zygote的子进程(由它fork出来的)。其中最重要的一个就是SystemServer,在ZygoteInit类的main方法中,会调用startSystemServer方法开启系统里面重要的服务,包括ActivityManagerService(Activity管理器,简称AMS,可以理解为一个服务进程,负责Activity的生命周期管理)、PackageManagerService(包管理器)、WindowManagerService(窗口管理器)、PowerManagerService(电量管理器)等等,这个过程中还会创建系统上下文。

     

    public final class SystemServer {
    
        //zygote的主入口
        public static void main(String[] args) {
            new SystemServer().run();
        }
    
        public SystemServer() {
            // Check for factory test mode.
            mFactoryTestMode = FactoryTest.getMode();
        }
        
        private void run() {
            
            ...ignore some code...
            
            //加载本地系统服务库,并进行初始化 
            System.loadLibrary("android_servers");
            nativeInit();
            
            // 创建系统上下文
            createSystemContext();
            
            //初始化SystemServiceManager对象,下面的系统服务开启都需要调用SystemServiceManager.startService(Class<T>),这个方法通过反射来启动对应的服务
            mSystemServiceManager = new SystemServiceManager(mSystemContext);
            
            //开启服务
            try {
                startBootstrapServices();
                startCoreServices();
                startOtherServices();
            } catch (Throwable ex) {
                Slog.e("System", "******************************************");
                Slog.e("System", "************ Failure starting system services", ex);
                throw ex;
            }
           
            ...ignore some code...
        
        }
    
        //初始化系统上下文对象mSystemContext,并设置默认的主题,mSystemContext实际上是一个ContextImpl对象。调用ActivityThread.systemMain()的时候,会调用ActivityThread.attach(true),而在attach()里面,则创建了Application对象,并调用了Application.onCreate()。
        private void createSystemContext() {
            ActivityThread activityThread = ActivityThread.systemMain();
            mSystemContext = activityThread.getSystemContext();
            mSystemContext.setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
        }
    
        //在这里开启了几个核心的服务,因为这些服务之间相互依赖,所以都放在了这个方法里面。
        private void startBootstrapServices() {
            
            ...ignore some code...
            
            //初始化ActivityManagerService
            mActivityManagerService = mSystemServiceManager.startService(
                    ActivityManagerService.Lifecycle.class).getService();
            mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
            
            //初始化PowerManagerService,因为其他服务需要依赖这个Service,因此需要尽快的初始化
            mPowerManagerService = mSystemServiceManager.startService(PowerManagerService.class);
    
            // 现在电源管理已经开启,ActivityManagerService负责电源管理功能
            mActivityManagerService.initPowerManagement();
    
            // 初始化DisplayManagerService
            mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);
        
        //初始化PackageManagerService
        mPackageManagerService = PackageManagerService.main(mSystemContext, mInstaller,
           mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
        
        ...ignore some code...
        
        }
    
    }

     

    注意一个很重要的地方

     private void createSystemContext() {
            ActivityThread activityThread = ActivityThread.systemMain();
            mSystemContext = activityThread.getSystemContext();
                      mSystemContext.setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
        }

    这里干了三件事,一个是创建ActivityThread,这个ActivityThread就是我们常说的“主线程”,也就是所谓的UI线程,APP的主入口。ActivityThread随后会创建一个mainLooper来开启消息循环,这也就是为什么在"主线程"中我们使用Handler不需要手动创建Looper的原因。第二件事是获得了系统的上下文,第三件事是设置了默认的主题。如果想要启动新的应用,ActivityManagerService会通过socket进程间通信(IPC)机制来通知Zygote进程fork出新的进程。

     

     

    二、当我们点击了一个APP图标时发生了什么

     

    系统第一个启动的APP是Lancher,也就是我们手机的主界面,继承自Activity,实现了点击事件、触摸、长按等接口,在android源码Lancher.java中,我们可以看到onclick方法

     

    public void onClick(View v) {
        
              ...ignore some code...
                
             Object tag = v.getTag();
            if (tag instanceof ShortcutInfo) {
                // Open shortcut
                final Intent intent = ((ShortcutInfo) tag).intent;
                int[] pos = new int[2];
                v.getLocationOnScreen(pos);
                intent.setSourceBounds(new Rect(pos[0], pos[1],
                        pos[0] + v.getWidth(), pos[1] + v.getHeight()));
            //开始开启Activity
                boolean success = startActivitySafely(v, intent, tag);
    
                if (success && v instanceof BubbleTextView) {
                    mWaitingForResume = (BubbleTextView) v;
                    mWaitingForResume.setStayPressed(true);
                }
            } else if (tag instanceof FolderInfo) {
                //如果点击的是图标文件夹,就打开文件夹
                if (v instanceof FolderIcon) {
                    FolderIcon fi = (FolderIcon) v;
                    handleFolderClick(fi);
                }
            } else if (v == mAllAppsButton) {
            ...ignore some code...
            }
        }

    可以看出我们点击了主界面上的应用图标后调用的就是startActivitySafely这个方法,继续深入进去,然后再往下走,我们发现调用的是startActivity方法,最后会调用Instrumentation.execStartActivity(),Instrumentation这个类就是完成对Application和Activity初始化和生命周期的工具类,然后Instrumentation会通过ActivityManagerService的远程接口向AMS发消息,让他启动一个Activity。 也就是说调用startActivity(Intent)以后, 会通过Binder IPC机制, 最终调用到ActivityManagerService。AMS会通过socket通道传递参数给Zygote进程。Zygote孵化自身, 并调用ZygoteInit.main()方法来实例化ActivityThread对象并最终返回新进程的pid。ActivityThread随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环。在ActivityThread会创建并绑定Application,这个时候才会realStartActivity(),并且AMS会将生成的Activity加到ActivityTask的栈顶,并通知ActivityThread暂停当前Activity(暂停Lancher,进入我们自己的APP)。

    三、Application在何处初始化

    ActivityThread.main()中,有一句话是thread.attach(false),在这个attach方法中,有一句比较重要的地方
     
    mgr.attachApplication(mAppThread)
    这个就会通过Binder调用到AMS里面对应的方法,继续研究源码,在handleBindApplication中,完成了Application的创建
    在makeApplication方法中,最后调用的是instrumentation.callApplicationOnCreate(app);
    这个方法里面的onCreate就是调用了我们Application的OnCreate方法。
    public Application makeApplication(boolean forceDefaultAppClass,
                Instrumentation instrumentation) {
            if (mApplication != null) {
                return mApplication;
            }
    
            Application app = null;
    
            String appClass = mApplicationInfo.className;
            if (forceDefaultAppClass || (appClass == null)) {
                appClass = "android.app.Application";
            }
    
            try {
                java.lang.ClassLoader cl = getClassLoader();
                if (!mPackageName.equals("android")) {
                    initializeJavaContextClassLoader();
                }
                ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
                app = mActivityThread.mInstrumentation.newApplication(
                        cl, appClass, appContext);
                appContext.setOuterContext(app);
            } catch (Exception e) {        }
            mActivityThread.mAllApplications.add(app);
            mApplication = app;
    
        //传进来的是null,所以这里不会执行,onCreate在上一层执行
            if (instrumentation != null) {
                try {
                    instrumentation.callApplicationOnCreate(app);
                } catch (Exception e) {
                   
                }
            }
            ...ignore some code... 
                  
           }
    
            return app;
        }
    上面的三点,用一张图来改概括,就是

    四、什么地方我们可以进行优化

    首先需要明确的是,在Application的onCreate之前,这个时间都是无法进行优化的,因为这部分是系统替我们完成的,开发者无能为力。所以我们能做的,就是从Application的onCreate方法开始,到lauyout_main.xml第一次布局绘制完成之前,来进行启动时间的优化。
     

    五、冷启动与热启动的区别

    1冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化 Application 类,再创建和初始化 MainActivity 类,最后显示在界面上。


    2  热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。热启动因为会从已有的进程中来启动,所以热启动就不会走 Application 这步了,而是直接走 MainActivity,所以热启动的过程不必创建和初始化 Application,因为一个应用从新进程的创建到进程的销毁,Application 只会初始化一次。
     

    六、时间定义

    由于冷启动过程中,系统和APP的许多工作都要重新开始,所以一般而言这种启动方式是最慢且最具有挑战性的。除了创建和初始化Application和MainActivity之外,冷启动还要加载主题样式Theme,inflate布局,setContentView ,测量、布局、绘制以后显示,我们才看到了屏幕的第一帧。
     
    1.DisplayTime
    API19以后,Android在系统Log中增加了Display的Log信息,在Android Studio中运行我们的App后,我们可以在Logcat中过滤ActivityManager以及Display这两个关键字,可以看到
    这个DisplayTime的Log信息在activity 窗口完成所有的启动事件之后,第一次绘制的时候输出。这个时间包括了从Activity启动到Layout全部显示的过程。这基本上就是你需要优化的主要时间。它不包含用户点击app图标然后系统开始准备启动activity的时间,因为作为一个开发者你无法影响这个时间,所以没有必要去测量它。
     
    2.reportFullyDrawn
     
    系统日志中的Display Time只是布局的显示时间,并不包括数据的加载,因为很多App在加载时会使用懒加载模式,即数据拉取后,再刷新默认的UI。所以,系统给我们定义了一个类似的『自定义上报时间』——reportFullyDrawn。
     
    如下图:
     
    3.三个time
     
    在cmd中输入adb shell am start -W [包名]/[全类名],即通过adb启动我们的Activity或Service,控制台也会输出我们的启动时间
     
     
    此法获取的启动时间非常精准,可精确到毫秒。那么这三个Thistime,TotalTime,WaitTime分别代表什么呢?
    • ThisTime: 最后一个启动的Activity的启动耗时
    • 自己的所有Activity的启动耗时
    • WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)
    这三个时间不是很好理解,我们可以把整个过程分解

    1.上一个Activity的onPause()——2.系统调用AMS耗时——3.第一个Activity(也许是闪屏页)启动耗时——4.第一个Activity的onPause()耗时——5.第二个Activity启动耗时

    那么,ThisTime表示5(最后一个Activity的启动耗时)。TotalTime表示3.4.5总共的耗时(如果启动时只有一个Activity,那么ThisTime与TotalTime就是一样的)。WaitTime则表示所有的操作耗时,即1.2.3.4.5所有的耗时。
     
    如果还是没有理解,可以查看framework层ActivityRecord源码,其中有这几个time的计算方法,
    最关键的几行代码
     public void reportFullyDrawnLocked() {
            final long curTime = SystemClock.uptimeMillis();
            if (displayStartTime != 0) {
                reportLaunchTimeLocked(curTime);
            }
            final ActivityStack stack = task.stack;
            if (fullyDrawnStartTime != 0 && stack != null) {
                final long thisTime = curTime - fullyDrawnStartTime;
                final long totalTime = stack.mFullyDrawnStartTime != 0
                        ? (curTime - stack.mFullyDrawnStartTime) : thisTime;
    }
    逻辑如下图

    七、为什么启动时会出现短暂黑屏或白屏的现象

    系统进程在创建Application的过程中会产生一个BackgroudWindow,等到App完成了第一次绘制,系统进程才会用MainActivity的界面替换掉原来的BackgroudWindow,见下图
     
    也就是说当用户点击你的app那一刻到系统调用Activity.onCreate()之间的这个时间段内,WindowManager会先加载app主题样式中的windowBackground做为app的预览元素,然后再真正去加载activity的layout布局。
    很显然,如果你的application或activity启动的过程太慢,导致系统的BackgroundWindow没有及时被替换,就会出现启动时白屏或黑屏的情况(取决于你的主题是Dark还是Light)。

    八、冷启动优化

    1.主题替换
    我们在style中自定义一个样式Lancher,在其中放一张背景图片,或是广告图片之类的
     
    <style name="AppTheme.Launcher">
            <item name="android:windowBackground">@drawable/bg</item>
        </style>
    把这个样式设置给启动的Activity
     
    <activity
                android:name=".activity.SplashActivity"
                android:screenOrientation="portrait"
                android:theme="@style/AppTheme.Launcher"
                >
    然后在Activity的onCreate方法,把Activity设置回原来的主题
     
    @Override
        protected void onCreate(Bundle savedInstanceState) {
            //替换为原来的主题,在onCreate之前调用
            setTheme(R.style.AppTheme);
            super.onCreate(savedInstanceState);
        }

     

    这样在启动时就通过给用户看一张图片或是广告来防止黑白屏的尴尬。

     

     
    还一种方式,就是把windowBackground属性设为null,这样在启动时,backgroundWindow的背景就会变成透明的,给人的感觉就是点了应用图标以后,延迟了一会儿然后加载第一个activity的界面。
    <style name="AppTheme.Launcher">
            <item name="android:windowBackground">@null</item>
        </style>
    2.优化Application和MainActivity
     
    上面所说的改变主题实际上是一种伪优化,因为它实质上并没有真正减少App启动的时间。
     
    Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,不得不说,各种第三方SDK,都特别喜欢这个『兵家必争之地』,再加上自己的一些库的初始化,会让整个Application不堪重负。优化的方法,无非是通过以下几个方面:
    • 延迟初始化
    • 后台任务
    • 界面预加载
     
    在Application的构造器方法、attachBaseContext()、onCreate()方法中不要进行耗时操作的初始化,一些数据预取放在异步线程中。
     
    数据库,IO操作,密集网络请求不要放在Application的构造方法中,能使用工作线程的尽量使用工作线程,不要在Application的onCreate中创建线程池,因为那样会有比较大的开销,可以考虑延后再创建。
    第三方SDK如果主线程中没有立即使用,可以考虑延迟几秒再初始化,总之一句话,尽早让用户看到应用的界面,其他操作都可以先让路。
     
    对于MainActivity,由于在获取到第一帧前,需要对contentView进行测量布局绘制操作,尽量减少布局的层次,考虑StubView的延迟加载策略,当然在onCreate、onStart、onResume方法中避免做耗时操作。
     
    对于sharedPreferences的初始化,因为sharedPreferences的特性在初始化时候会对数据全部读出来存在内存中,所以这个初始化放在主线程中不合适,反而会延迟应用的启动速度,对于这个还是需要放在异步线程中处理。 
     
    例子
    new Thread(){
                @Override
                public void run() {
                    initNim();
                    initImagePicker();
                    initOkHttp();
                }
            }.start();

     

    九、优化启动时间的一个很好用的工具 TraceView

    TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。
     
    用法很简单,在你想要测试花费时间的代码之前,调用Debug.startMethodTracing(filename),系统会生产 trace 文件,并且产生追踪数据,在结束处调用代码Debug.stopMethodTracing()时,会将追踪数据写入到 trace 文件中。以下代码在sd卡中生成trace文件
     
    File file = new File(Environment.getExternalStorageDirectory(), "app7");
           Log.i(TAG, "onCreate: " + file.getAbsolutePath());
           Debug.startMethodTracing(file.getAbsolutePath());
            //对全局属性赋值
            mContext = getApplicationContext();
            mMainThread = Thread.currentThread();
            mMainThreadId = android.os.Process.myTid();
            mMainLooper = getMainLooper();
            mHandler = new Handler();
            initNim();
            initImagePicker();
            initOkHttp();
            Debug.stopMethodTracing();
    如果你用的是模拟器,就可以使用下面的控制台命令将trace文件拉出到桌面上
    cd Desktop
    adb pull /storage/sdcard/app7.trace
    然后将这个trace文件拖入android studio 就可以了,可以看到这样的界面


    具体各个部分的信息如下
       
    从上半部分可以清晰的看出方法之间的调用关系和运行时间,从下半部分可以具体看出那几个方法耗时较长
    有了这样方便的工具,启动优化时对症下药就很方便了
     
    也欢迎关注个人公众号,移动开发,音视频,图形图像,大厂内推,不定时更新

     
    参考博客
     
     
     
     
    展开全文
  • Elasticsearch:写入原理谈写入优化

    千次阅读 2021-04-06 00:17:33
    写入优化十三:推荐使用官方客户端 推荐使用官方 Elasticsearch API,因为官方在连接池和保持连接状态方面优化。 高版本 JAVA API 推荐:官方的High-level-Rest API。 其他写入优化 待补充...... 6、写入过程中...

    1、线上实战问题

    问题 1:想要请问一下,我这边需求是每分钟利用 sparksteaming 插入按天的索引150万条数据。一般情况下还好,索引7个分片,1副本,但是偶尔会出现延迟很高的情况。比如:一般情况下1分钟插入150万能正常插入,可能突然就出现了需要5分钟才能插入成功,然后又正常了。很头疼。

    请问这种情况我需要怎么去查看一下是否正常。我已经把副本设置成了0,还把批量插入的参数从 5000 设置成 2 万。我节点是 12 个 16g 的,但是好像还是没有改观。

    问题 2:由于使用了多个分词器的原因造成数据写入慢,请问有什么优化的方法吗?

    问题 3:求问:现在日志 收集链路 kafka-logstash-es,压力测试 logstash输出70M/s,而 Elasticsearch 索引写入一半不到。这边的性能损失可能是什么原因呢?需要怎么调优呢?

    类似问题还有很多、很多......

    2、问题分析

    以上三个问题各有各自特点,但基本都是基于不同的数据源向 Elasticsearch 写入过程中遇到的问题。

    可以简单归结为:Elasticsearch 写入问题或者写入优化问题。Elasticsearch 写入问题涉及写入流程、写入原理及优化策略。

    本文针对如上几点展开讨论。

    3、基于单文档/批量文档写入流程谈写入优化

    单个文档写入对应 Index 请求,批量写入对应 Bulk 请求,Index 和 Bulk 是相同的处理逻辑,请求统一封装到 BulkRequest中。

    流程拆解如下:

    第一:客户端向 Node 1 发送写数据请求。

    注意,此时Node1 便充当协调节点(cooridiniate)的角色。

    第二:节点Node1 使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。

    使用文档的 _id 确定文档所属分片的方法是:路由算法。

    路由算法计算公式:

    shard = hash(routing) % number_of_primary_shards
    
    • routing:文档 _id。

    • number_of_primary_shards: 主分片个数。

    • shard: 文档 _id 所属分片号。

    第三:Node 3 在主分片上面执行写入操作。如果写入成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告写入成功,协调节点向客户端报告写入成功。

    如上流程拆解后的注意点:

    • 写操作必须在主分片执行成功后,才能复制到相关的副本分片。

    • 主分片写入失败,则整个请求被认为是写失败。

    • 如果有部分副本写失败(前提:主分片写入成功),则整个请求被认为是写成功。

    如果设置了副本,数据会先写入主分片,主分片再同步到副本分片,同步操作会加重磁盘 IO,间接影响写入性能。

    基于以上分析,既然主分片的写入起到写入成败的决定性作用。那么写入前将:副本分片写入前置为0,待写入完成后复原副本,是不是就能提升写入性能了呢?

    是的!

    写入优化一:副本分片写入前置为0,等完成写入后复原副本

    PUT test-0001
    {
      "settings": {
        "number_of_replicas": 0
      }
    }
    

    写入优化二:优先使用系统自动生成 id

    文档的_id 的生成有两种方式,

    • 第一:系统自动生成id。

    • 第二:外部控制自增id。

    但,如果使用外部 id,Elasticsearch 会先尝试读取原来文档的版本号,以判断是否需要更新。

    也就是说,使用外部控制 id 比系统自动生成id要多一次读取磁盘操作。

    所以,非特殊场景推荐使用系统自动生成的 id。

    4、基于 Elasticsearch 写入原理谈写入优化

    Elasticsearch 中的 1 个索引由一个或多个分片组成,每个分片包含多个segment(段),每一个段都是一个倒排索引。如下图所示:

    在 lucene 中,为了实现高索引速度,使用了segment 分段架构存储。一批写入数据保存在一个段中,其中每个段最终落地为磁盘中的单个文件。

    将文档插入 Elasticsearch 时,它们会被写入缓冲区中,然后在刷新时定期从该缓冲区刷新到段中。刷新频率由 refresh_interval 参数控制,默认每1秒刷新一次。

    也就是说,新插入的文档在刷新到段(内存中)之前,是不能被搜索到的。如下图所示:

    刷新的本质是:写入数据由内存 buffer 写入到内存段中,以保证搜索可见。

    来看个例子,加深对 refresh_inteval 的理解,注释部分就是解读。

    PUT test_0001/_doc/1
    {
      "title":"just testing"
    }
    
    # 默认一秒的刷新频率,秒级可见(用户无感知)
    
    GET test_0001/_search
    

    如下设置后,写入后 60s 后才可见。

    DELETE test_0001
    # 设置了60s的刷新频率
    PUT test_0001
    {
      "settings": {
        "index":{
          "refresh_interval":"60s"
        }
      }
    }
     
    PUT test_0001/_doc/1
    {
      "title":"just testing"
    }
    # 60s后才可以被搜索到
    GET test_0001/_search
    

    关于是否需要实时刷新:

    • 如果新插入的数据需要近乎实时的搜索功能,则需要频繁刷新。

    • 如果对最新数据的检索响应没有实时性要求,则应增加刷新间隔,以提高数据写入的效率。

    所以,自然我们想到的优化是:调整刷新频率。

    写入优化三:合理调整刷新频率

    调整方法如下:

    方法1:写入前刷新频率设置为 -1,写入后设置为业务实际需要值(比如:30s)。

    PUT test-008
    {
      "settings": {
        "refresh_interval": -1
      }
    }
    

    方法2:直接设置为业务实际需要值(比如:30s)

    PUT test-008
    {
      "settings": {
        "refresh_interval": "30s"
      }
    }
    

    写入优化四:合理调整堆内存中索引缓冲区(index_buffer)大小

    堆内存中 index_buffer 用于存储新索引的文档。

    填满后,缓冲区中的文档将最终写入磁盘上的某个段。

    index_buffer_size 默认值如下所示,为堆内存的 10%。

    indices.memory.index_buffer_size: 10%
    

    例如,如果给 JVM 31GB的内存,它将为索引缓冲区提供 3.1 GB的内存,一般情况下足以容纳大量数据的写入操作。

    但,如果着实数据量非常大,建议调大该默认值。比如:调整为堆内存的 20%。

    调整建议:必须在集群中的每个数据节点上进行配置。

    缓存区越大,意味着能缓存数据量越大,相同时间段内,写盘频次低、磁盘 IO 小,间接提升写入性能。

    写入优化五:给堆外内存也留够空间(常规要求)

    这其实算不上写入优化建议,而是通用集群配置的常规配置。

    内存分配设置堆内存比例官方建议:机器内存大小一半,但不超过 32 GB。

    一般设置建议:

    • 如果内存大小 >= 64 GB,堆内存设置:31 GB。

    • 如果内存大小 < 64 GB,堆内存设置:内存大小一半。

    堆内存之外的内存留给:Lucene 使用。

    推荐阅读:干货 | 吃透Elasticsearch 堆内存

    写入优化六:bulk 批量写入而非单个文档写入

    批量写入自然会比单个写入性能要好(批量写入意味着相同时间产生的段会大,段的总个数自然会少),但批量值的设置一般需要慎重,不要盲目一下搞的很大。

    一般建议:递增步长测试,直到达到资源使用上限。

    比如:第一次批量值设置:100,第二次:200,第三次:400,以此类推......

    批量值 bulk 已经 ok 了,但集群尚有富余资源,资源利用并没有饱和怎么办?

    上多线程,通过并发提升写入性能。

    写入优化七:多线程并发写入

    这点,在 logstash 同步数据到 Elasticsearch,基于spark、kafka、Flink 批量写入 Elasticsearch时,经常会出现:Bulk Rejections 的报错。

    当批量请求到达集群中的某个节点时,整个请求将被放入批量队列中,并由批量线程池中的线程进行处理。批量线程池处理来自队列的请求,并将文档转发到副本分片,作为此处理的一部分。子请求完成后,将响应发送到协调节点。

    Elasticsearch 具有有限大小的请求队列的原因是:为了防止集群过载,从而增加了稳定性和可靠性。

    如果没有任何限制,客户端可以很容易地通过恶意攻击行为将整个集群搞宕机。

    这里就引申出下面的优化点。

    写入优化八:合理设置线程池和队列大小

    关于线程池和队列,参考:Elasticsearch 线程池和队列问题,请先看这一篇

    核心建议就是:结合 CPU 核数和 esrally 的测试结果谨慎的调整 write 线程池和队列大小。

    为什么要谨慎设置?

    针对批量写入拒绝(reject)的场景,官方建议:

    增加队列的大小不太可能改善集群的索引性能或吞吐量。相反,这只会使集群在内存中排队更多数据,这很可能导致批量请求需要更长的时间才能完成。

    队列中的批量请求越多,将消耗更多的宝贵堆空间。如果堆上的压力太大,则可能导致许多其他性能问题,甚至导致集群不稳定。

    推荐阅读:

    https://www.elastic.co/cn/blog/why-am-i-seeing-bulk-rejections-in-my-elasticsearch-cluster

    5、其他写入优化建议

    写入优化九:设置合理的Mapping

    实战业务场景中不推荐使用默认 dynamic Mapping,一定要手动设置 Mapping。

    • 举例1:默认字符串类型是:text 和 keyword 的组合类型,就不见得适用所有业务场景。要结合自己业务场景设置,正文 cont 文本内容一般不需要设置 keyword 类型(因为:不需要排序和聚合操作)。

    • 举例2:互联网采集数据存储场景,正文需要全文检索,但包含 html 样式的文本一般留给前端展示用,不需要索引。这时候Mapping 设置阶段要果断将 index 设置为 false。

    写入优化十:合理的使用分词器

    分词器决定分词的粒度,常见的中文分词 IK 可细分为:

    • 粗粒度分词:ik_smart。

    • 细粒度分词:ik_max_word。

    从存储角度基于 ik_max_word 分词的索引会比基于 ik_smart 分词的索引占据空间大。

    而更细粒度自定义分词 ngram 会占用大量资源,并且可能减慢索引速度并显着增加索引大小。

    所以要结合检索指标(召回率和精准率)、结合写入场景斟酌选型。

    写入优化十一:必要时,使用 SSD 磁盘

    SSD 很贵,但很香。

    尤其针对写入密集型场景,如果其他优化点都考虑了,这可能是你最后一根“救命稻草“。

    写入优化十二:合理设置集群节点角色

    这也是经常被问到的问题,集群规模小的时候,一般节点会混合多种角色,如:主节点 + 数据节点、数据节点 + 协调节点混合部署。

    但,集群规模大了之后,硬件资源相对丰富后,强烈建立:独立主节点、独立协调节点。

    让各个角色的节点各尽其责,对写入、检索性能都会有帮助。

    写入优化十三:推荐使用官方客户端

    推荐使用官方 Elasticsearch API,因为官方在连接池和保持连接状态方面有优化。

    高版本 JAVA API 推荐:官方的High-level-Rest API。

    其他写入优化

    待补充......

    6、写入过程中做好监控

    如下是 kibana 监控截图,其中:index Rate 就是写入速率。

    • index rate: 每秒写入的文档数。

    • search rate:每秒的查询次数(分片级别,非请求级别),也就意味着一次查询请求命中的分片数越多,值越大。

    7、小结

    Elasticsearch 写入优化没有普适的最优优化建议,只有适合自己业务场景的反复试验、调优,形成属于自己业务场景的最佳实践。

    你的业务场景做过哪些写入优化?欢迎留言讨论交流。


    参考:

    • https://www.elastic.co/guide/en/elasticsearch/reference/7.2/tune-for-indexing-speed.html

    • 《Elasticsearch源码解析与优化实战》

    • https://wx.zsxq.com/dweb2/index/search/%E5%88%86%E7%89%87%E6%95%B0/alltopics

    • https://t.zsxq.com/v7qNNRz

    • https://opster.com/blogs/improve-elasticsearch-indexing-rate/

    • 国外博客



    推荐:

    展开全文
  • 数据库索引原理优化

    千次阅读 2019-03-07 09:20:00
    下面详细介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。 存存取原理 目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却具体差别,抽象...
  • HashMap原理分析及JDK1.8性能优化

    千次阅读 2018-01-23 17:33:44
     当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子,随着map的size越来越...
  • 主成分分析原理

    千次阅读 2014-06-18 15:00:37
    所对应的成分,那么所得的低维度数据必定是最优化的(也即,这样降低维度必定是失去讯息最少的方法)。主成分分析分析复杂数据时尤为有用,比如 人脸识别 。 PCA是最简单的以特征量分析多元统计分布的方法。...
  • JVM原理优化

    万次阅读 多人点赞 2013-08-27 18:04:15
    JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置 2.装载JVM.dll 3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例 4.调用...
  • Lucene底层原理优化经验分享:Lucene简介和索引原理
  • 最优化理论与方法-牛顿迭代法

    千次阅读 2018-04-06 22:21:21
    关注微信公众号【Microstrong】,我 现在研究方向是机器学习、...最优化问题中,牛顿法为什么比梯度下降法求解需要的迭代次数更少? - 大饼土博的回答 - 知乎 https://www.zhihu.com/question/19723347/answer/14636244
  • 起源:在2015年,Seyedali Mirjalili学者受到自然规律启发,根据飞蛾飞行时的导航机制,在模拟飞蛾螺旋飞行的路径中提出一种新型群智能优化算法:飞蛾火焰优化算法(moth-flame optimization,MFO)。 定义:飞蛾...
  • 基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB级、万亿条数据记录的检索工作,这里向... Lucene简介和索引原理 该部分方面展开:Lucene简介、索引原理、Lucene索引实现。1.1 Lucene简介 Lucen
  • Lucene底层原理优化经验分享(2)-Lucene优化经验总结

    万次阅读 热门讨论 2017-01-06 09:21:45
    Lucene优化也一样,找到性能瓶颈,找对解决方法,才能事半功倍,本文将方面阐述我们的Lucene优化经验:  1. 找准方向 -> Lucene性能瓶颈分析。  2. 找对方法 -> Lucene代码架构分析。  3. 方法落地 -> ...
  • 是的,Netty框架就是为了高并发而生的,由于并发访问的数量很大,一点点的性能优化,就会带来可观的性能提升效应,Netty主要如下两个方面对ThreadLocal的实现进行优化 线程对象直接提供 set、get方法,以便直接...
  • 摘要: 第一部分:基础知识 第二部分:MYISAM和INNODB索引结构 1、 简单介绍B-tree B+ tree树 2、 MyisAM索引结构 ...第三部分:MYSQL优化 ...2、sql语句优化 ...(1) 左前缀原则
  • Android 性能优化四个方面总结

    千次阅读 2018-12-17 16:56:49
    目录一、四个方面二、卡顿优化1、Android系统显示原理2、卡顿根本原因3、性能分析工具(1)Profile GPU Rendering(2)TraceView(3)Systrace UI 性能分析4、优化建议(1)布局优化(2)避免过度绘制(3)启动优化...
  • Hive原理及查询优化

    万次阅读 多人点赞 2016-05-21 12:24:26
    Hive是构建在Hadoop上的数据仓库软件框架,支持使用SQL来读,写和管理大规模...Hive具有目前Hadoop上丰富全的SQL语法,也拥有稳定的执行。是目前Hadoop上几乎标准的ETL和数据仓库工具。 Hive这个特点与
  • 数据库索引底层原理优化

    千次阅读 多人点赞 2018-09-11 14:11:18
    下面详细介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。 3.2 主存存取原理 目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却...
  • Android性能优化方面解析

    千次阅读 2017-05-15 11:47:59
    一、性能优化。二、高级UI。三、JNI/NDK开发。四、架构师。五、RN开发。这也许将会是我的进阶趋势。早已知道在瓶颈期的我,似乎看到了突破的希望的。初级进阶中级也好,中级进阶高级也罢,现在的市场无非是根据经验...
  • HashMap实现原理分析

    万次阅读 多人点赞 2013-11-05 15:23:28
     首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这...
  • 又称帕累托分析法或巴雷托分析法、柏拉图分析、主次因分析法 、ABC分析法、分类管理法、物资重点管理法、ABC管理法、abc管理、巴雷特分析法,平常我们也称之为“80对20”规则,EMBA、MBA等主流商管教育均对ABC分类法...
  • SQL优化最干货总结 - MySQL(2020最新版)

    万次阅读 多人点赞 2020-06-29 16:55:47
    MySQL - SQL优化干货总结(吐血版),别辜负了自己的梦想,欢迎白嫖、点赞、收藏。
  • 当然,有些时候(或者说大部分时候)数据并不是线性可分的,这个时候满足这样条件的超平面就根本不存在(不过关于如何处理这样的问题我们后面会讲),这里先从最简单的情形开始推导,就假设数据都是线性可分的,亦即...
  • Hive原理及查询优化(杨卓荦)

    千次阅读 2018-05-25 16:49:42
    今天,我想和大家简单介绍一下Hive原理和查询优化。由于时间有限,很多内容简要介绍一下,欢迎私下多交流。Hive是构建在Hadoop上的数据仓库软件框架,支持使用SQL来读,写和管理大规模数据集合。Hive入门非常简单,...
  • 最优化理论学习———问题汇总

    千次阅读 2016-08-07 16:31:11
    最优化理论学习概念总结
  • [性能优化] - cpu调度原理

    千次阅读 2018-08-01 18:13:00
    [性能优化] - cpu调度原理 1 背景 2 基本概念 3 CPU调度程度 3.2 O(1)调度程序 3.3完全公平调度程序CFS 4 CPU与性能优化 5 总结 [性能优化] - cpu调度原理 second60 20180801   1 背景 linux系统是一...
  • 最优化方法的Matlab实现

    万次阅读 多人点赞 2007-01-29 20:23:00
    现在看一段 摄像机标定的程序里面涉及到最优化求解方程的函数,网上找到了下面的资源,只是里面的公式显示不出来,贴在这里,做为工具查阅,如果找到原文的出处,再做修改。在生活和工作中,人们对于同一个问题...
  • MySQL查询优化器工作原理解析

    万次阅读 2016-05-28 21:06:31
    手册上MYSQL查询优化器概述;个人对MySQL优化器的理解;分析优化优化过程中的信息;调节MySQL优化器的优化

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 155,206
精华内容 62,082
关键字:

从哪些方面分析原理的最优化