Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 男女mm视频,日本中文在线观看,亚洲日本欧美产综合在线

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          OpenResty+Lua限流實(shí)戰(zhàn)

          penResty+Lua限流實(shí)戰(zhàn)

          當(dāng)業(yè)務(wù)量越來(lái)越大的時(shí)候,為了能保證服務(wù)的運(yùn)行,限流是必不可少的!OpenResty是一個(gè)高性能網(wǎng)關(guān)

          OpenResty? is a dynamic web platform based on NGINX and LuaJIT.

          OpenResty = Nginx + Lua,Lua是高性能腳本語(yǔ)言,有著C語(yǔ)言的執(zhí)行效率但是又比C簡(jiǎn)單,能很方便的擴(kuò)展OpenResty 的功能。

          Lua 是由巴西里約熱內(nèi)盧天主教大學(xué)(Pontifical Catholic University of Rio de Janeiro)里的一個(gè)研究小組于1993年開發(fā)的一種輕量、小巧的腳本語(yǔ)言,用標(biāo)準(zhǔn) C 語(yǔ)言編寫,其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。

          官網(wǎng):http://www.lua.org/

          實(shí)戰(zhàn)環(huán)境

          docker + CentOS8 + Openresty 1.17.8.2

          Lua限流模塊

          https://github.com/openresty/lua-resty-limit-traffic

          Lua的庫(kù)一般都是小巧輕便且功能都具備,這個(gè)限流庫(kù)核心文件一共就四個(gè),幾百行代碼就能實(shí)現(xiàn)限流功能,Lua的其他庫(kù)也是這樣,比如redis的庫(kù)還是Http的庫(kù),麻雀雖小五臟俱全!

          環(huán)境準(zhǔn)備

          docker run -dit --name gw  --privileged centos /usr/sbin/init
          docker exec -it gw bash 

          在gw中

          # 安裝openresty
          yum install -y yum-utils
          yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
          yum install -y openresty
          
          # 安裝工具等
          yum install -y net-tools vim telnet git httpd
          
          # Openresty自帶了lua-resty-limit-traffic組件,如果沒有帶,下載到/usr/local/openresty/lualib/resty/limit/文件夾即可
          # 下載lua-resty-limit-traffic組件
          [ `ls /usr/local/openresty/lualib/resty/limit/ | wc -l` = 0 ] &&  echo '請(qǐng)安裝限速組件' || echo '已經(jīng)安裝限速組件'
          # 安裝了請(qǐng)忽略
          cd ~ && git clone https://github.com/openresty/lua-resty-limit-traffic.git
          mkdir -p /usr/local/openresty/lualib/resty/limit/
          cp  lua-resty-limit-traffic/lib/resty/limit/*.lua /usr/local/openresty/lualib/resty/limit/
          
          # 啟動(dòng)openresy
          openresty

          限并發(fā)

          場(chǎng)景:按照 ip 限制其并發(fā)連

          參考: https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/lua-limit.html https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn.md https://developer.aliyun.com/article/759299

          原理:lua_share_dict是nginx所有woker和lua runtime共享的,當(dāng)一個(gè)請(qǐng)求來(lái),往lua_share_dict記錄鍵值對(duì)ip地址:1,當(dāng)請(qǐng)求完成時(shí)再-1,再來(lái)一個(gè)在+1,設(shè)置一個(gè)上限5,當(dāng)超過(guò)5時(shí)則拒絕請(qǐng)求,一定要注意內(nèi)部重定向的問(wèn)題!

          • OpenResty執(zhí)行階段 tag:lua執(zhí)行流程;執(zhí)行階段;openresty執(zhí)行流程
          • 為啥access_by_lua執(zhí)行兩次

          環(huán)境搭建

          新建utils/limit_conn.lua模塊

          mkdir -p /usr/local/openresty/lualib/utils
          cat > /usr/local/openresty/lualib/utils/limit_conn.lua <<EOF
          -- utils/limit_conn.lua
          local limit_conn = require "resty.limit.conn"
          
          -- new 的第四個(gè)參數(shù)用于估算每個(gè)請(qǐng)求會(huì)維持多長(zhǎng)時(shí)間,以便于應(yīng)用漏桶算法
          local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)
          if not limit then
              error("failed to instantiate a resty.limit.conn object: ", limit_err)
          end
          
          local _M = {}
          
          function _M.incoming()
              local key = ngx.var.binary_remote_addr
              local delay, err = limit:incoming(key, true)
              if not delay then
                  if err == "rejected" then
                      return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
                  end
                  ngx.log(ngx.ERR, "failed to limit req: ", err)
                  return ngx.exit(500)
              end
          
              if limit:is_committed() then
                  local ctx = ngx.ctx
                  ctx.limit_conn_key = key
                  ctx.limit_conn_delay = delay
              end
          
              if delay >= 0.001 then
                  ngx.log(ngx.WARN, "delaying conn, excess ", delay,
                          "s per binary_remote_addr by limit_conn_store")
                  ngx.sleep(delay)
              end
          end
          
          function _M.leaving()
              local ctx = ngx.ctx
              local key = ctx.limit_conn_key
              if key then
                  local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
                  local conn, err = limit:leaving(key, latency)
                  if not conn then
                      ngx.log(ngx.ERR,
                      "failed to record the connection leaving ",
                      "request: ", err)
                  end
              end
          end
          
          return _M
          
          EOF

          重點(diǎn)在于這句話local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05),允許的最大并發(fā)為常規(guī)的8個(gè),突發(fā)的2個(gè),一共8+2=10個(gè)并發(fā),詳情參考https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn.md#new

          被拒絕的請(qǐng)求直接返回503

          if err == "rejected" then
              return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
          end

          修改nginx配置文件

          # 備份一下配置文件
          cd /usr/local/openresty/nginx/conf/ && \cp nginx.conf nginx.conf.bak
          
          # 添加配置
          echo '' > /usr/local/openresty/nginx/conf/nginx.conf
          vim /usr/local/openresty/nginx/conf/nginx.conf

          添加如下內(nèi)容

          worker_processes  1;
          
          events {
              worker_connections  1024;
          }
          http {
              include       mime.types;
              default_type  application/octet-stream;
              sendfile        on;
              keepalive_timeout  65;
              lua_code_cache on;    
             
              # 注意 limit_conn_store 的大小需要足夠放置限流所需的鍵值。
              # 每個(gè) $binary_remote_addr 大小不會(huì)超過(guò) 16 字節(jié)(IPv6 情況下),算上 lua_shared_dict 的節(jié)點(diǎn)大小,總共不到 64 字節(jié)。
              # 100M 可以放 1.6M 個(gè)鍵值對(duì)
              lua_shared_dict limit_conn_store 100M;
              
              server {
                  listen 80;
                  location / {
                      access_by_lua_block {
                          local limit_conn = require "utils.limit_conn"
                          -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                          if ngx.req.is_internal() then
                              ngx.log(ngx.INFO,">> 內(nèi)部重定向")
                              return
                          end
                          limit_conn.incoming()
                          ngx.log(ngx.INFO,">>> 請(qǐng)求進(jìn)來(lái)了!")
                      }
                      content_by_lua_block {
                          -- 模擬請(qǐng)求處理時(shí)間,很重要,不加可能測(cè)試不出效果
                          -- 生產(chǎn)中沒有請(qǐng)求是只返回一個(gè)靜態(tài)的index.html的!
                          ngx.sleep(0.5)
                      }
                      log_by_lua_block {
                          local limit_conn = require "utils.limit_conn"
                          limit_conn.leaving()
                          ngx.log(ngx.INFO,">>> 請(qǐng)求離開了!")
                      }
                      
                  }
              }
          }
          

          重點(diǎn)在于這句話,模擬每個(gè)請(qǐng)求0.5秒處理完成

          content_by_lua_block {
              ngx.sleep(0.5)
          }
          

          注意在限制連接的代碼里面,我們用 ngx.ctx 來(lái)存儲(chǔ) limit_conn_key。這里有一個(gè)坑。內(nèi)部重定向(比如調(diào)用了 ngx.exec)會(huì)銷毀 ngx.ctx,導(dǎo)致 limit_conn:leaving() 無(wú)法正確調(diào)用。 如果需要限連業(yè)務(wù)里有用到 ngx.exec,可以考慮改用 ngx.var 而不是 ngx.ctx,或者另外設(shè)計(jì)一套存儲(chǔ)方式。只要能保證請(qǐng)求結(jié)束時(shí)能及時(shí)調(diào)用 limit:leaving() 即可。

          重新加載配置文件

          openresty -s reload 

          測(cè)試

          上面的配置是每個(gè)請(qǐng)求處理0.5秒,并發(fā)是10

          • 10個(gè)請(qǐng)求,并發(fā)為1
          ab -n 10 -c 1  127.0.0.1/
          
          # 請(qǐng)求全部成功,用時(shí)5s左右
          Concurrency Level:      1
          Time taken for tests:   5.012 seconds 
          Complete requests:      10 
          Failed requests:        0
          
          • 10個(gè)請(qǐng)求,并發(fā)為10
          ab -n 10 -c 10  127.0.0.1/
          
          # 請(qǐng)求全部成功,用時(shí)1.5s左右
          Concurrency Level:      10
          Time taken for tests:   1.505 seconds
          Complete requests:      10
          Failed requests:        0
          
          • 20個(gè)請(qǐng)求,并發(fā)為10,并發(fā)為10并不會(huì)觸發(fā)限制條件,所以能成功!注意和下面并發(fā)11的區(qū)別!
          ab -n 20 -c 10  127.0.0.1/
          
          # 請(qǐng)求全部成功,用時(shí)2s左右
          Concurrency Level:      10
          Time taken for tests:   2.005 seconds
          Complete requests:      20
          Failed requests:        0
          • 22個(gè)請(qǐng)求,并發(fā)為11 重點(diǎn)解釋一下:并發(fā)不是qps,并發(fā)11不是說(shuō)第一秒發(fā)11個(gè)請(qǐng)求,然后第二秒再發(fā)送11個(gè)請(qǐng)求,而是發(fā)完第一波緊接著發(fā)第二波,每一波的間隔時(shí)間不一定是1秒,下面的1.506 seconds就能看出來(lái),按理應(yīng)該是2s但是并不是第一波11個(gè)請(qǐng)求發(fā)送過(guò)去了,但是只能處理10個(gè),所以成功了10個(gè),緊接著第二波11個(gè)請(qǐng)求發(fā)過(guò)去了,但是第一波大部分未處理完成所以第二波的都失敗了,也有處理完成了的可以接著處理,所以至少會(huì)成功10個(gè),下面顯示的是11個(gè)此處的大量失敗應(yīng)該是并發(fā)超過(guò)了10,觸發(fā)了限制條件讓nginx worker線程睡眠了,所以導(dǎo)致后面的請(qǐng)求大量失敗-- 觸發(fā)限制條件
            if delay >= 0.001 then
            ngx.sleep(delay) -- ngx worker睡眠
            end
          ab -n 22 -c 11  127.0.0.1/
          
          # 11個(gè)成功,11個(gè)失敗
          Concurrency Level:      11
          Time taken for tests:   1.506 seconds
          Complete requests:      22
          Failed requests:        11
          Non-2xx responses:      11 # HTTP狀態(tài)非2xx的有11個(gè),說(shuō)明限并發(fā)成功(只有有非2xx的返回才會(huì)顯示這句話)

          反向代理

          上面測(cè)試的是content_by_lua,也就是內(nèi)容直接在lua中生成,但是實(shí)際中內(nèi)容有可能是后端服務(wù)器生成的,所以可以設(shè)置反向代理或者負(fù)載均衡,如下為反向代理配置

          location / {
              access_by_lua_block {
                  local limit_conn = require "utils.limit_conn"
                  -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                  if ngx.req.is_internal() then
                      return
                  end
                  limit_conn.incoming()
              }
              log_by_lua_block {
                  local limit_conn = require "utils.limit_conn"
                  limit_conn.leaving()
              }
              
              # 反向代理
              proxy_pass http://172.17.0.3:8080;
              proxy_set_header Host $host;
              proxy_redirect off;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_connect_timeout 60;
              proxy_read_timeout 600;
              proxy_send_timeout 600;
          
          }
          

          內(nèi)部重定向

          location / {
            access_by_lua_block {...}
            content_by_lua_block {...}
            log_by_lua_block {...}
          }

          nginx是按照階段來(lái)執(zhí)行指令的,和配置文件順序沒有關(guān)系,nginx是先執(zhí)行access_by_lua_block,再執(zhí)行content_by_lua_block,最后執(zhí)行log_by_lua_block的,當(dāng)在訪問(wèn)curl 127.0.0.1/時(shí),如果沒有content_by_lua_block,這里有一個(gè)內(nèi)部重定向,會(huì)將127.0.0.1/的請(qǐng)求重定向到127.0.0.1/index.html,所以會(huì)按順序再次執(zhí)行access_by_lua_block,所以access_by_lua_block執(zhí)行了兩次,log_by_lua_block卻執(zhí)行了一次,當(dāng)時(shí)的我十分懵逼,而加上content_by_lua或者proxy_pass則不會(huì)導(dǎo)致重定向,總之有內(nèi)容來(lái)源時(shí)不會(huì)重定向,沒有則會(huì)去找index.html導(dǎo)致重定向!

          測(cè)試

          vim /usr/local/openresty/nginx/conf/nginx.conf
          
          # 修改成如下內(nèi)容
          server {
            listen 80;
            location / {
                access_by_lua_block {
                    ngx.log(ngx.ERR,">>> access")
                }
                log_by_lua_block {
                    ngx.log(ngx.ERR,">>> log")
                }
            }
          }
          
          # 查看日志
          tail -f /usr/local/openresty/nginx/logs/error.log
          • 測(cè)試curl 127.0.0.1日志輸出如下 access_by_lua_block執(zhí)行了兩次,并且頁(yè)面上的內(nèi)容是index.html的內(nèi)容,說(shuō)明發(fā)生了重定向 如果加上index.html,即curl 127.0.0.1/index.html,則不會(huì)發(fā)生重定向
          ...[lua] access_by_lua(nginx.conf:24):2: >>> access, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "127.0.0.1"
          ...[lua] access_by_lua(nginx.conf:24):2: >>> access, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "127.0.0.1"
          ...[lua] log_by_lua(nginx.conf:27):2: >>> log while logging request, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "127.0.0.1"
          • 加上content_by_lua則訪問(wèn)http://127.0.0.1不會(huì)發(fā)生重定向

          lua初始化

          這句話local limit_conn = require "utils.limit_conn"limit_conn中的local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)只會(huì)初始化一次,之后都是用的都一個(gè)實(shí)例,不會(huì)每個(gè)請(qǐng)求進(jìn)來(lái)都要new一個(gè)limit_conn有點(diǎn)浪費(fèi)性能而且還把參數(shù)都重置了,是不可取的,所以封裝到了utils.limit_conn中!

          限制接口時(shí)間窗請(qǐng)求數(shù)(非平滑)

          場(chǎng)景:限制 ip 每1s只能調(diào)用 10 次(允許在時(shí)間段開始的時(shí)候一次性放過(guò)10個(gè)請(qǐng)求)也就是說(shuō),速率不是固定的

          也可以設(shè)置成別的,比如120/min,只需要修改個(gè)數(shù)和時(shí)間窗口(resty.limit.countresty.limit.req區(qū)別在于:前者傳入的是個(gè)數(shù),后者傳入的是速率)

          新建utils/limit_count.lua模塊

          mkdir -p /usr/local/openresty/lualib/utils
          cat > /usr/local/openresty/lualib/utils/limit_count.lua <<EOF
          -- utils/limit_count.lua
          local limit_count = require "resty.limit.count"
          
          -- rate:  10/s
          local lim, err = limit_count.new("my_limit_count_store", 10, 1) -- 第二個(gè)參數(shù)次數(shù),第三個(gè)參數(shù)時(shí)間窗口,單位s
          if not lim then
              ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err)
              return ngx.exit(500)
          end
          
          local _M = {}
          
          
          function _M.incoming()
              local key = ngx.var.binary_remote_addr
              local delay, err = lim:incoming(key, true)
              if not delay then
                  if err == "rejected" then
                      ngx.header["X-RateLimit-Limit"] = "10"
                      ngx.header["X-RateLimit-Remaining"] = 0
                      return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
                  end
                  ngx.log(ngx.ERR, "failed to limit req: ", err)
                  return ngx.exit(500)
              end
              
              -- 第二個(gè)參數(shù)是指定key的剩余調(diào)用量
              local remaining = err
          
              ngx.header["X-RateLimit-Limit"] = "10"
              ngx.header["X-RateLimit-Remaining"] = remaining
          
          end
          
          return _M
          
          EOF

          修改nginx配置文件

          echo '' > /usr/local/openresty/nginx/conf/nginx.conf
          vim /usr/local/openresty/nginx/conf/nginx.conf

          添加如下內(nèi)容

          worker_processes  1;
          
          events {
              worker_connections  1024;
          }
          http {
              include       mime.types;
              default_type  application/octet-stream;
              sendfile        on;
              keepalive_timeout  65;
              lua_code_cache on;    
             
              lua_shared_dict my_limit_count_store 100M;
              
              # resty.limit.count 需要resty.core
              init_by_lua_block {
                  require "resty.core"
              }
              
              server {
                  listen 80;
                  location / {
                      access_by_lua_block {
                          local limit_count = require "utils.limit_count"
                          -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                          if ngx.req.is_internal() then
                              return
                          end
                          limit_count.incoming()
                      }            
                      
                      content_by_lua_block {
                          ngx.sleep(0.1)
                          ngx.say('Hello')
                      }
                      # 如果內(nèi)容源是反向代理
                      #proxy_pass http://172.17.0.3:8080;
                      #proxy_set_header Host $host;
                      #proxy_redirect off;
                      #proxy_set_header X-Real-IP $remote_addr;
                      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      #proxy_connect_timeout 60;
                      #proxy_read_timeout 600;
                      #proxy_send_timeout 600;
          
                  }
              }
          }
          

          重新加載配置文件

          openresty -s reload 

          測(cè)試

          上面的配置是10/s,不疊加

          • 10個(gè)請(qǐng)求,并發(fā)為10,1s內(nèi)完成
          ab -n 10 -c 10  127.0.0.1/
          
          # 請(qǐng)求全部成功
          Concurrency Level:      10
          Time taken for tests:   0.202 seconds
          Complete requests:      10
          Failed requests:        0
          
          
          • 20個(gè)請(qǐng)求,并發(fā)為20,1s內(nèi)完成
          ab -n 20 -c 20  127.0.0.1/
          
          # 請(qǐng)求成功10個(gè),其余全部失敗
          Concurrency Level:      20
          Time taken for tests:   0.202 seconds
          Complete requests:      20
          Failed requests:        10
             (Connect: 0, Receive: 0, Length: 10, Exceptions: 0)
          Non-2xx responses:      10
          
          
          • 查看請(qǐng)求頭curl -I 127.0.0.1,可以看到接口限流信息
          
          HTTP/1.1 200 OK
          Server: openresty/1.17.8.2
          Date: Sat, 12 Sep 2020 09:46:06 GMT
          Content-Type: application/octet-stream
          Connection: keep-alive
          X-RateLimit-Limit: 10 # 當(dāng)前限制10個(gè)
          X-RateLimit-Remaining: 9 # 剩余9個(gè)
          

          限制接口時(shí)間窗請(qǐng)求數(shù)(平滑)

          桶(無(wú)容量)

          場(chǎng)景:限制 ip 每1min只能調(diào)用 120次(平滑處理請(qǐng)求,即每秒放過(guò)2個(gè)請(qǐng)求),速率是固定的,并且桶沒有容量(容量為0)

          新建utils/limit_req_bucket.lua模塊

          mkdir -p /usr/local/openresty/lualib/utils
          cat > /usr/local/openresty/lualib/utils/limit_req_bucket.lua <<EOF
          -- utils/limit_req_bucket.lua
          local limit_req = require "resty.limit.req"
          
          -- rate:  2/s即為120/min,burst設(shè)置為0,也就是沒有桶容量,超過(guò)的都拒絕(rejected)
          local lim, err = limit_req.new("my_limit_req_store", 2, 0)
          if not lim then
              ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
              return ngx.exit(500)
          end
          
          local _M = {}
          
          
          function _M.incoming()
              local key = ngx.var.binary_remote_addr
              local delay, err = lim:incoming(key, true)
              if not delay then
                  if err == "rejected" then
                      return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
                  end
                  ngx.log(ngx.ERR, "failed to limit req: ", err)
                  return ngx.exit(500)
              end
          end
          
          return _M
          
          EOF

          修改nginx配置文件

          echo '' > /usr/local/openresty/nginx/conf/nginx.conf
          vim /usr/local/openresty/nginx/conf/nginx.conf

          添加如下內(nèi)容

          worker_processes  1;
          
          events {
              worker_connections  1024;
          }
          http {
              include       mime.types;
              default_type  application/octet-stream;
              sendfile        on;
              keepalive_timeout  65;
              lua_code_cache on;    
             
              lua_shared_dict my_limit_req_store 100M;
              
              server {
                  listen 80;
                  location / {
                      access_by_lua_block {
                          local limit_count = require "utils.limit_req_bucket"
                          -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                          if ngx.req.is_internal() then
                              return
                          end
                          limit_count.incoming()
                      }            
                      
                      content_by_lua_block {
                          ngx.sleep(0.1)
                          ngx.say('Hello')
                      }
                      # 如果內(nèi)容源是反向代理
                      #proxy_pass http://172.17.0.3:8080;
                      #proxy_set_header Host $host;
                      #proxy_redirect off;
                      #proxy_set_header X-Real-IP $remote_addr;
                      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      #proxy_connect_timeout 60;
                      #proxy_read_timeout 600;
                      #proxy_send_timeout 600;
          
                  }
              }
          }
          

          重新加載配置文件

          openresty -s reload 

          測(cè)試

          上面的配置是2/s即為120/min

          • 請(qǐng)求時(shí)間限制為1s
          ab -t 1 127.0.0.1/
          
          # 實(shí)際請(qǐng)求1.1s,成功3個(gè)請(qǐng)求,符合預(yù)期
          Time taken for tests:   1.100 seconds
          Complete requests:      8656
          Failed requests:        8653
             (Connect: 0, Receive: 0, Length: 8653, Exceptions: 0)
          Non-2xx responses:      8653
          
          
          
          • 請(qǐng)求時(shí)間限制為5s
          ab -t 5 127.0.0.1/
          
          # 實(shí)際請(qǐng)求5.1s,成功11個(gè)請(qǐng)求,符合預(yù)期
          Concurrency Level:      1
          Time taken for tests:   5.100 seconds
          Complete requests:      40054
          Failed requests:        40043
             (Connect: 0, Receive: 0, Length: 40043, Exceptions: 0)
          Non-2xx responses:      40043

          漏桶(有桶容量)

          場(chǎng)景:限制 ip 每1min只能調(diào)用 120次(平滑處理請(qǐng)求,即每秒放過(guò)2個(gè)請(qǐng)求),速率是固定的,并且桶的容量有容量(設(shè)置burst)

          新建utils/limit_req_leaky_bucket.lua模塊

          只需要在桶(無(wú)容量)的基礎(chǔ)之上增加burst的值即可,并且增加delay的處理

          mkdir -p /usr/local/openresty/lualib/utils
          cat > /usr/local/openresty/lualib/utils/limit_req_leaky_bucket.lua <<EOF
          -- utils/limit_req_leaky_bucket.lua
          local limit_req = require "resty.limit.req"
          
          -- rate:  2/s即為120/min,增加桶容量為1/s,超過(guò)2/s不到(2+1)/s的delay,排隊(duì)等候,這就是標(biāo)準(zhǔn)的漏桶
          local lim, err = limit_req.new("my_limit_req_store", 2, 1)
          if not lim then
              ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
              return ngx.exit(500)
          end
          
          local _M = {}
          
          
          function _M.incoming()
              local key = ngx.var.binary_remote_addr
              local delay, err = lim:incoming(key, true)
              if not delay then
                  if err == "rejected" then
                      return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
                  end
                  ngx.log(ngx.ERR, "failed to limit req: ", err)
                  return ngx.exit(500)
              end
              
              -- 此方法返回,當(dāng)前請(qǐng)求需要delay秒后才會(huì)被處理,和他前面對(duì)請(qǐng)求數(shù)
              -- 所以此處對(duì)桶中請(qǐng)求進(jìn)行延時(shí)處理,讓其排隊(duì)等待,就是應(yīng)用了漏桶算法
              -- 此處也是與令牌桶的主要區(qū)別
              if delay >= 0.001 then
                  ngx.sleep(delay)
              end
          end
          
          return _M
          
          EOF
          
          

          修改nginx配置文件

          echo '' > /usr/local/openresty/nginx/conf/nginx.conf
          vim /usr/local/openresty/nginx/conf/nginx.conf

          添加如下內(nèi)容

          worker_processes  1;
          
          events {
              worker_connections  1024;
          }
          http {
              include       mime.types;
              default_type  application/octet-stream;
              sendfile        on;
              keepalive_timeout  65;
              lua_code_cache on;    
             
              lua_shared_dict my_limit_req_store 100M;
              
              server {
                  listen 80;
                  location / {
                      access_by_lua_block {
                          local limit_count = require "utils.limit_req_leaky_bucket"
                          -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                          if ngx.req.is_internal() then
                              return
                          end
                          limit_count.incoming()
                      }            
                      
                      content_by_lua_block {
                          -- 模擬每個(gè)請(qǐng)求的耗時(shí)
                          ngx.sleep(0.1)
                          ngx.say('Hello')
                      }
                      # 如果內(nèi)容源是反向代理
                      #proxy_pass http://172.17.0.3:8080;
                      #proxy_set_header Host $host;
                      #proxy_redirect off;
                      #proxy_set_header X-Real-IP $remote_addr;
                      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      #proxy_connect_timeout 60;
                      #proxy_read_timeout 600;
                      #proxy_send_timeout 600;
          
                  }
              }
          }
          

          重新加載配置文件

          openresty -s reload 

          測(cè)試

          上面的配置是2/s,漏桶容量為1/s,即總共3/s,模擬的每個(gè)請(qǐng)求耗時(shí)為0.1s,那么1s內(nèi)能處理至少10個(gè)請(qǐng)求

          • 請(qǐng)求時(shí)間限制為1s
          ab -t 1 127.0.0.1/
          
          # 實(shí)際請(qǐng)求1.102s,成功3個(gè)請(qǐng)求,1s兩個(gè)請(qǐng)求,一個(gè)是delay,符合預(yù)期
          Time taken for tests:   1.103 seconds
          Complete requests:      3
          Failed requests:        0

          令牌桶

          場(chǎng)景:限制 ip 每1min只能調(diào)用 120次(平滑處理請(qǐng)求,即每秒放過(guò)2個(gè)請(qǐng)求),但是允許一定的突發(fā)流量(突發(fā)的流量,就是桶的容量(桶容量為60),超過(guò)桶容量直接拒絕

          令牌桶其實(shí)可以看著是漏桶的逆操作,看我們對(duì)把超過(guò)請(qǐng)求速率而進(jìn)入桶中的請(qǐng)求如何處理,如果是我們把這部分請(qǐng)求放入到等待隊(duì)列中去,那么其實(shí)就是用了漏桶算法,但是如果我們?cè)试S直接處理這部分的突發(fā)請(qǐng)求,其實(shí)就是使用了令牌桶算法。

          這邊只要將上面漏桶算法關(guān)于桶中請(qǐng)求的延時(shí)處理的代碼修改成直接送到后端服務(wù)就可以了,這樣便是使用了令牌桶

          新建utils/limit_req_token_bucket.lua模塊

          mkdir -p /usr/local/openresty/lualib/utils
          cat > /usr/local/openresty/lualib/utils/limit_req_token_bucket.lua <<EOF
          -- utils/limit_req_token_bucket.lua
          local limit_req = require "resty.limit.req"
          
          -- rate:  2/s即為120/min,增加桶容量為60/s,超過(guò)2/s不到(2+60)/s的突發(fā)流量直接放行
          local lim, err = limit_req.new("my_limit_req_store", 2, 60)
          if not lim then
              ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
              return ngx.exit(500)
          end
          
          local _M = {}
          
          
          function _M.incoming()
              local key = ngx.var.binary_remote_addr
              local delay, err = lim:incoming(key, true)
              if not delay then
                  if err == "rejected" then
                      return ngx.exit(503) -- 超過(guò)的請(qǐng)求直接返回503
                  end
                  ngx.log(ngx.ERR, "failed to limit req: ", err)
                  return ngx.exit(500)
              end
              
              if delay >= 0.001 then
                  -- 不做任何操作,直接放行突發(fā)流量
                  -- ngx.sleep(delay)
              end
          end
          
          return _M
          
          EOF

          修改nginx配置文件

          echo '' > /usr/local/openresty/nginx/conf/nginx.conf
          vim /usr/local/openresty/nginx/conf/nginx.conf

          添加如下內(nèi)容

          worker_processes  1;
          
          events {
              worker_connections  1024;
          }
          http {
              include       mime.types;
              default_type  application/octet-stream;
              sendfile        on;
              keepalive_timeout  65;
              lua_code_cache on;    
             
              lua_shared_dict my_limit_req_store 100M;
              
              server {
                  listen 80;
                  location / {
                      access_by_lua_block {
                          local limit_count = require "utils.limit_req_token_bucket"
                          -- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
                          if ngx.req.is_internal() then
                              return
                          end
                          limit_count.incoming()
                      }            
                      
                      content_by_lua_block {
                          -- 模擬每個(gè)請(qǐng)求的耗時(shí)
                          ngx.sleep(0.1)
                          ngx.say('Hello')
                      }
                      # 如果內(nèi)容源是反向代理
                      #proxy_pass http://172.17.0.3:8080;
                      #proxy_set_header Host $host;
                      #proxy_redirect off;
                      #proxy_set_header X-Real-IP $remote_addr;
                      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      #proxy_connect_timeout 60;
                      #proxy_read_timeout 600;
                      #proxy_send_timeout 600;
          
                  }
              }
          }
          

          重新加載配置文件

          openresty -s reload 

          測(cè)試

          上面模擬的每個(gè)請(qǐng)求耗時(shí)為0.1s,那么1s內(nèi)能處理至少10個(gè)請(qǐng)求

          • 時(shí)間限制為1s
          ab -n 10 -c 10  -t 1 127.0.0.1/
          
          # 實(shí)際請(qǐng)求1s,成功13個(gè)請(qǐng)求,可以看到是遠(yuǎn)遠(yuǎn)超過(guò)2個(gè)請(qǐng)求的,多余就是在處理突發(fā)請(qǐng)求
          Concurrency Level:      10
          Time taken for tests:   1.000 seconds
          Complete requests:      12756
          Failed requests:        12743
             (Connect: 0, Receive: 0, Length: 12743, Exceptions: 0)
          Non-2xx responses:      12743
          

          組合各種limter

          上面的三種限速器conn、count、req可以進(jìn)行各種組合,比如一個(gè)限速器是限制主機(jī)名的,一個(gè)是限制ip的,可以組合起來(lái)使用

          參考:https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/traffic.md

          習(xí) Lua 的必要性

          因?yàn)楣ぷ髦薪?jīng)常與 nginx 打交道,而 nginx 又有大量的模塊是由 Lua 寫的,所以有必要學(xué)習(xí)下 Lua 基礎(chǔ)的語(yǔ)法知識(shí)。Lua 作為一門動(dòng)態(tài)腳本語(yǔ)言,解釋執(zhí)行,和 JavaScript 有點(diǎn)相似。

          語(yǔ)言特點(diǎn)

          1. 語(yǔ)句結(jié)束沒有分號(hào)
          2. 跟 JavaScript 很像
          3. 默認(rèn)定義的是全局變量,定義局部變量需要加 local 關(guān)鍵字
          4. 數(shù)組索引從1開始
          5. 沒有 i++ 操作符號(hào),只能 i = i + 1

          注釋

          1. 單行注釋
          2. -- 注釋內(nèi)容
          3. 多行注釋
          4. --[[
          5. 注釋內(nèi)容
          6. ]]--

          內(nèi)置數(shù)據(jù)類型

          總共有6種內(nèi)置數(shù)據(jù)類型, 其中包括nil, boolean, number, string, table, function

          1. nil
          2. 通常是沒有賦值,直接使用才會(huì)是這個(gè)值, 比如說(shuō)下面的代碼直接打印 變量 name
          3. lua print(name)
          4. 在 ifelse 判斷語(yǔ)句中,nil 被當(dāng)成false 分支,但 nil ~= false, 在 Lua 語(yǔ)言當(dāng)中,不等于使用 ~= 來(lái)表示, 而不是我們常見的 != 。
          5. boolean
          6. 有兩種取值:true, false
          7. number
          8. 所有的數(shù)值類型都使用 number 來(lái)表示,不管是整數(shù),還是浮點(diǎn)數(shù),其實(shí)內(nèi)部的存儲(chǔ)方式是雙精度類型。
          9. string
          10. 字符串可以用雙引號(hào),也可以用單引號(hào)包圍起來(lái),特殊字符需要轉(zhuǎn)義
          name = "dev4mobile"
          name = 'dev4mobile'
          nameWithAge = 'dev4mobile \n 25'
          
          1. 多行字符串
           welcome = [[
           hello world
           ]]
          
          1. table
          2. 其實(shí)就是其它語(yǔ)言里面的對(duì)象, 有兩種表現(xiàn)方式,一種是數(shù)組,一種是字典(Map),
          3. 都是使用大括號(hào)括起來(lái)的。記住數(shù)組索引從1開始。
          arr = { 1, "dev4mobile", 'cn.dev4mobile@gamil.com', 12.3, function()endv}
          person = { name = 'dev4mobile' }
          
          1. function
          2. 定義如下,以 function關(guān)鍵字作為開頭,add 是函數(shù)名字
           -- 一般定義
           function add(a, b)
           return a + b
           end
           -- 傳遞多個(gè)參數(shù)
           funcation print(...)
           print(...)
           end
           -- 返回多個(gè)參數(shù)
           function()
           return "abc", 12, function() end
           end
          

          控制流語(yǔ)句

          1. 循環(huán)
          2. 循環(huán)有3種寫法,for, while,repeat .. until
          3. 說(shuō)明: #變量名 表示讀取變量的長(zhǎng)度,可以是字符串和數(shù)組
          -- for 循環(huán)
          arr = { 1, 2, 3, 4, 5 }
          for i=1, #arr do -- 索引從1開始
           print(arr[i])
          end
          -- while 循環(huán)
          arr = { 1, 2, 3, 4, 5 }
          i = 1
          while i <= #arr do
          print(arr[i])
          i = i + 1
          end
          -- repeate until 循環(huán)
          arr = { 1, 2, 3, 4, 5 }
          i = 1
          repeat
          print(arr[i])
          i = i + 1
          until i >= #arr
          
          1. 分支 ( ifelse )
          name = "dev4mobile"
          if #name > 10 then
           print("name length = ".. #name)
          elseif #name >5 then
           print("name length > 5, real length = "..#name) -- 兩個(gè)點(diǎn)..代表字符串
          else 
           print("name length < "..#name)
          end
          

          面向?qū)ο?/strong>

          實(shí)現(xiàn)原理:有點(diǎn)類似 JavaScript 的實(shí)現(xiàn)使用原型方式,使用函數(shù) + table 實(shí)現(xiàn)。

          • 模塊
          • 在寫demo之前有必要先介紹下模塊的概念,一般來(lái)說(shuō)一個(gè)文件就是一個(gè)模塊,跟 JavaScript 一樣, 導(dǎo)入模塊關(guān)鍵字 require, 導(dǎo)出模塊關(guān)鍵字return
          • 下面我們來(lái)新建一個(gè)模塊名
          • 首先新建一個(gè)文件名: perosn.lua,輸入下面代碼

          眾所周知,內(nèi)存的高低是評(píng)判一款app的性能優(yōu)劣的重要的指標(biāo)之一。如何更簡(jiǎn)單的幫助開發(fā)者分析、暴露且解決內(nèi)存泄漏問(wèn)題,幾乎是每一個(gè)平臺(tái)或框架、開發(fā)者亟需的一個(gè)的"標(biāo)配"的feature。但是對(duì)于flutter社區(qū),缺少一款用得順手的內(nèi)存泄漏工具。

          對(duì)于使用flutter而言,因使用dart語(yǔ)言,通過(guò)形成渲染樹提交到c++的skia進(jìn)行渲染,從dart層到c++層擁有很長(zhǎng)的渲染鏈路,使用者必須對(duì)整個(gè)渲染鏈路有通盤深刻的理解,才能深刻此時(shí)此刻的內(nèi)存使用情況。本文提出一種基于渲染樹個(gè)數(shù)的方式尋找內(nèi)存泄漏的解決方案。

          flutter內(nèi)存都包含哪些

          虛擬內(nèi)存還是物理內(nèi)存?

          當(dāng)我們談?wù)搩?nèi)存時(shí),通常說(shuō)的是物理內(nèi)存(Physical memory),同一個(gè)應(yīng)用程序運(yùn)行在不同機(jī)器或者操作系統(tǒng)上時(shí),會(huì)因不同操作系統(tǒng)和機(jī)器的硬件條件的不同,分配的到物理內(nèi)存大小會(huì)有所不同,但大致而言,一款應(yīng)用程序所使用到的虛擬內(nèi)存(Virtual Memory)而言便會(huì)大致一樣,本文討論的都指的是虛擬內(nèi)存。

          我們可以直觀的理解,代碼中操作的所有對(duì)象都是能用虛擬內(nèi)存衡量,而不太關(guān)心對(duì)象是否存在于物理內(nèi)存與否,只要能減少對(duì)象的應(yīng)用,盡量少的持有對(duì)象,不管白貓黑貓,能減少對(duì)象的,都是“好貓”。

          討論flutter內(nèi)存時(shí),我們?cè)谡務(wù)撌裁?/strong>

          flutter從使用的語(yǔ)言上,可以分成3大部分,

          • Framework層 由Dart編寫,開發(fā)者接觸到頂層,用于應(yīng)用層開發(fā)

          • Engine 層,由C/C++編寫,主要進(jìn)行圖形渲染

          • Embedder層,由植入層語(yǔ)言編寫,如iOS使用Objective-C/swift,Android使用java

          當(dāng)我們從進(jìn)程角度談?wù)揻lutter應(yīng)用的內(nèi)存時(shí),指的是這個(gè)三者所有的內(nèi)存的總和。

          為簡(jiǎn)化,這里可以簡(jiǎn)單的以使用者能直接接觸的代碼為邊界,將其分成DartVM和native內(nèi)存, DartVM指Dart虛擬機(jī)占用內(nèi)存,而native內(nèi)存包含Engine和平臺(tái)相關(guān)的代碼運(yùn)行的內(nèi)存。

          既然說(shuō)Flutter的使用者能接觸到的最直接的對(duì)象都是使用Dart語(yǔ)言生成的對(duì)象,那么對(duì)于Engine層的對(duì)象的創(chuàng)建與銷毀,使用者似乎鞭長(zhǎng)莫及了?這就不得不說(shuō)Dart虛擬機(jī)綁定層的設(shè)計(jì)了。

          Dart綁定層如何工作

          出于性能或者跨平臺(tái)或其他原因,腳本語(yǔ)言或者基于虛擬機(jī)的語(yǔ)言都會(huì)提供c/c++或函數(shù)對(duì)象綁定到具體語(yǔ)言對(duì)象的接口,以便在語(yǔ)言中接著操控c/c++對(duì)象或函數(shù),這層API稱為綁定層。例如: 最易嵌入應(yīng)用程序中的Lua binding ,Javascript V8 引擎的binding 等等。

          Dart虛擬機(jī)在初始化時(shí),會(huì)將C++聲明的某個(gè)類或者函數(shù)和某個(gè)函數(shù)和Dart中的某個(gè)類或者綁定起來(lái),依次注入Dart運(yùn)行時(shí)的全局遍歷中,當(dāng)Dart代碼執(zhí)行某一個(gè)函數(shù)時(shí),便是指向具體的C++對(duì)象或者函數(shù)。

          下面是幾個(gè)常見的綁定的幾個(gè)c++類和對(duì)應(yīng)的Dart類

          flutter::EngineLayer --> ui.EngineLayer

          flutter::FrameInfo --> ui.FrameInfo

          flutter::CanvasImage --> ui.Image

          flutter::SceneBuilder --> ui.SceneBuilder

          flutter::Scene --> ui.Scene

          ui.SceneBuilder一個(gè)例子了解下Dart是如何綁定c++對(duì)象實(shí)例,并且控制這個(gè)c++實(shí)例的析構(gòu)工作。

          Dart層渲染過(guò)程是配置的layer渲染樹,并且提交到c++層進(jìn)行渲染的過(guò)程。ui.SceneBuilder便是這顆渲染樹的容器

          1. Dart代碼調(diào)用構(gòu)造函數(shù) ui.SceneBuilder時(shí),調(diào)用c++方法SceneBuilder_constructor

          2. 調(diào)用 flutter::SceneBuilder的構(gòu)造方法并生成c++實(shí)例sceneBuilder

          3. flutter::SceneBuilder繼承自內(nèi)存計(jì)數(shù)對(duì)象RefCountedDartWrappable,對(duì)象生成后會(huì)內(nèi)存計(jì)數(shù)加1

          4. 將生成c++實(shí)例sceneBuilder使用Dart的API生成一個(gè) WeakPersitentHandle,注入到Dart上下中。在這里之后,Dart便可使用這個(gè)builder對(duì)象,便可操作這個(gè)c++的flutter::SceneBuilder實(shí)例。

          5. 程序運(yùn)行許久后,當(dāng)Dart虛擬機(jī)判斷Dart 對(duì)象builder沒有被任何其他對(duì)象引用時(shí)(例如簡(jiǎn)單的情況是被置空builder=,也稱為無(wú)可達(dá)性),對(duì)象就會(huì)被垃圾回收器(Garbage Collection)回收釋放,內(nèi)存計(jì)數(shù)將會(huì)減一

          6. 當(dāng)內(nèi)存計(jì)數(shù)為0時(shí),會(huì)觸發(fā)c++的析構(gòu)函數(shù),最終c++實(shí)例指向的內(nèi)存塊被回收

          可以看到,Dart是通過(guò)將C/C++實(shí)例封裝成WeakPersitentHandle且注入到Dart上下文的方式,從而利用Dart虛擬機(jī)的GC(Garbage Collection)來(lái)控制C/C++實(shí)例的創(chuàng)建和釋放工作

          更直白而言,只要C/C++實(shí)例對(duì)應(yīng)的Dart對(duì)象能正常被GC回收,C/C++所指向的內(nèi)存空間便會(huì)正常釋放。

          WeakPersistentHandle是什么

          因?yàn)镈art對(duì)象在VM中會(huì)因?yàn)镚C整理碎片化中經(jīng)常移動(dòng),所以使用對(duì)象時(shí)不會(huì)直接指向?qū)ο螅鞘褂镁浔╤andle)的方式間接指向?qū)ο螅僬遚/c++對(duì)象或者實(shí)例是介乎于Dart虛擬機(jī)之外,生命周期不受作用域約束,且一直長(zhǎng)時(shí)間存在于整個(gè)Dart虛擬機(jī)中,所以稱為常駐(Persistent),所以WeakPersistentHandle專門指向生命周期與常在的句柄,在Dart中專門用來(lái)封裝C/C++實(shí)例。

          在flutter官方提供的Observatory工具中,可以查看所有的WeakPersistentHandle對(duì)象

          其中Peer這欄也就是封裝c/c++對(duì)象的指針

          Dart對(duì)象的可達(dá)性

          Dart對(duì)象釋放會(huì)被垃圾回收器(Garbage Collection)進(jìn)行釋放,是通過(guò)判定對(duì)象是否還有可達(dá)性(availability)來(lái)達(dá)到的。可達(dá)性是指通過(guò)某些根節(jié)點(diǎn)出發(fā),通過(guò)對(duì)象與對(duì)象間的引用鏈去訪問(wèn)對(duì)象,如可通過(guò)引用鏈去訪問(wèn)對(duì)象,則說(shuō)明對(duì)象有可達(dá)性,否則無(wú)可達(dá)性。

          黃色有可達(dá)性,藍(lán)色無(wú)可達(dá)性

          難以察覺的內(nèi)存泄漏

          看到這里我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,其實(shí)我們很難從Dart側(cè)感知C/C++對(duì)象的消亡,因?yàn)镈art對(duì)象無(wú)統(tǒng)一的如同C++類一樣的析構(gòu)函數(shù),一旦對(duì)象因?yàn)檠h(huán)引用等的原因被長(zhǎng)期其他對(duì)象長(zhǎng)期引用,GC將無(wú)法將其釋放,最終導(dǎo)致內(nèi)存泄漏。

          將問(wèn)題放大一點(diǎn),我們知道flutter是一個(gè)渲染引擎,我們通過(guò)編寫Dart語(yǔ)言構(gòu)建出一顆Widget樹,進(jìn)而經(jīng)過(guò)繪制等過(guò)程簡(jiǎn)化成Element樹,RenderObject樹,Layer樹,并將這顆Layer樹提交至C++層,進(jìn)而使用Skia進(jìn)行渲染。

          如果某個(gè)Wigdet樹或Element樹的某個(gè)節(jié)點(diǎn)長(zhǎng)期無(wú)法得到釋放,將可能造成他的子節(jié)點(diǎn)也牽連著無(wú)法釋放,將泄漏的內(nèi)存空間迅速擴(kuò)大。

          例如,存在兩個(gè)A,B界面,A界面通過(guò)Navigator.push的方式添加B界面,B界面通過(guò)Navigator.pop回退到A。如果B界面因?yàn)槟承懛ǖ木壒蕦?dǎo)致B的渲染樹雖然被從主渲染樹解開后依然無(wú)法被釋放,這會(huì)導(dǎo)致整個(gè)原來(lái)B的子樹都無(wú)法釋放。

          通過(guò)檢測(cè)渲染樹節(jié)點(diǎn)來(lái)檢測(cè)內(nèi)存泄漏

          基于上面的這一個(gè)情況,我們其實(shí)可以通過(guò)對(duì)比當(dāng)前幀使用到的渲染節(jié)點(diǎn)個(gè)數(shù),對(duì)比當(dāng)前內(nèi)存中渲染節(jié)點(diǎn)的個(gè)數(shù)來(lái)判斷前一個(gè)界面釋放存在內(nèi)存泄漏的情況。

          Dart代碼中都是通過(guò)往 ui.SceneBuilder添加EngineLayer的方式去構(gòu)建渲染樹,那么我們只要檢測(cè)c++中內(nèi)存中EngineLayer的個(gè)數(shù),對(duì)比當(dāng)前幀使用的EngineLayer個(gè)數(shù),如果內(nèi)存中的EngineLayer個(gè)數(shù)長(zhǎng)時(shí)間大于使用的個(gè)數(shù),那么我們可以判斷存在有內(nèi)存泄漏

          依然以上次A頁(yè)面pushB界面,B界面pop回退A界面為例子。正常無(wú)內(nèi)存泄漏的情況下,正在使用的layer個(gè)數(shù)(藍(lán)色),內(nèi)存中的layer個(gè)數(shù)(橙色)兩條曲線的雖然有波動(dòng),但是最終都會(huì)比較貼合。

          但是在B頁(yè)面存在內(nèi)存泄漏的時(shí)候,退到A界面后,B樹完全無(wú)法釋放,內(nèi)存中的layer個(gè)數(shù)(橙色)無(wú)法最終貼合藍(lán)色曲線(正在使用的layer個(gè)數(shù))

          也就是說(shuō),對(duì)于渲染而言,如果代碼導(dǎo)致Widget樹或Element樹長(zhǎng)時(shí)間無(wú)法被GC回收,很可能會(huì)導(dǎo)致嚴(yán)重的內(nèi)存泄漏情況。

          什么導(dǎo)致了內(nèi)存泄漏?

          目前發(fā)現(xiàn)異步執(zhí)行的代碼的場(chǎng)景(Feature, async/await,methodChan)長(zhǎng)期持有傳入的BuildContext,導(dǎo)致 element 被移除后,依然長(zhǎng)期存在,最終導(dǎo)致以及關(guān)聯(lián)的 widget, state 發(fā)生泄漏。

          再繼續(xù)看B頁(yè)面泄漏的例子

          正確與錯(cuò)誤的寫法的區(qū)別在于,錯(cuò)誤的僅是在調(diào)用Navigator.pop之前,使用異步方法Future引用了BuildContext,便會(huì)導(dǎo)致B界面內(nèi)存泄漏。

          怎么發(fā)現(xiàn)泄漏點(diǎn)?

          目前flutter內(nèi)存泄漏檢測(cè)工具的設(shè)計(jì)思路是,對(duì)比界面進(jìn)入前后的對(duì)象,尋找出未被釋放的對(duì)象,進(jìn)而查看未釋放的引用關(guān)系(Retaining path或Inbound references),再結(jié)合源碼進(jìn)行分析,最后找到錯(cuò)誤代碼。

          使用Flutter自帶的Observatory縱然可以一個(gè)一個(gè)查看每個(gè)泄漏對(duì)象的引用關(guān)系,但是對(duì)于一個(gè)稍微復(fù)雜一點(diǎn)的界面而言,最終生成的layer個(gè)數(shù)是非常龐雜的,想要在Observatory所有的泄漏對(duì)象中找到有問(wèn)題的代碼是一項(xiàng)非常龐雜的任務(wù)。

          為此我們將這些繁雜的定位工作都進(jìn)行了可視化。

          我們這里將每一幀提交到engine的所有EngineLayer進(jìn)行了一個(gè)記錄,并且以折線圖的形式記錄下來(lái),如果上文說(shuō)的內(nèi)存中的layer個(gè)數(shù)異常的大于使用中的layer個(gè)數(shù),那么就可判斷前一個(gè)頁(yè)面存在有內(nèi)存泄漏。

          進(jìn)而,還可以抓取當(dāng)前頁(yè)面的layer樹的結(jié)構(gòu),用以輔助定位具體由哪個(gè)RenderObject樹生成的Layer樹,進(jìn)而繼續(xù)分析由哪個(gè)Element節(jié)點(diǎn)生成的RenderObject節(jié)點(diǎn)

          或者也可以打印出WeakPersitentHandle的引用鏈輔助分析

          但如今的痛點(diǎn)依然存在,依然需要通過(guò)查看Handle的引用鏈,結(jié)合源碼的分析才能最終比較快捷的定位問(wèn)題。這也是接下來(lái)亟需解決的問(wèn)題。

          總結(jié)

          • 我們這種從渲染樹的角度去探尋flutter內(nèi)存泄漏的方法,可以推廣到所以其他Dart不同類型的對(duì)象。

          • 開發(fā)者在編寫代碼時(shí),需要時(shí)刻注意異步調(diào)用,以及時(shí)刻注意操縱的Element會(huì)否被引用而導(dǎo)致無(wú)法釋放

          閑魚作為長(zhǎng)期深耕flutter的團(tuán)隊(duì),也在持續(xù)在flutter工具鏈中持續(xù)發(fā)力,當(dāng)然也少不了這一重要的內(nèi)存檢測(cè)工具的深入開發(fā),歡迎大家持續(xù)關(guān)注!


          主站蜘蛛池模板: 国产在线观看一区精品| 国模精品一区二区三区视频| 波多野结衣中文字幕一区| 一区二区在线观看视频| 手机看片一区二区| 中文字幕一区二区三区在线观看| 无码人妻视频一区二区三区 | 波多野结衣中文一区| 日韩精品电影一区| 视频一区二区中文字幕| 国产一区二区三区精品视频| 视频在线观看一区二区| 亚洲成人一区二区| 国产精品丝袜一区二区三区| 日韩精品无码一区二区三区| 性无码免费一区二区三区在线| 国产亚洲一区二区在线观看| 久久久一区二区三区| 韩国美女vip福利一区| 在线视频国产一区| 国产精品一区12p| 一区高清大胆人体| 亚洲色一区二区三区四区| 日韩人妻无码免费视频一区二区三区| 国产精品无码一区二区三区不卡| 波多野结衣一区二区三区高清av | 欧洲精品免费一区二区三区 | 男人的天堂精品国产一区| 国产精品被窝福利一区| 精品一区二区三区无码免费视频| 波多野结衣久久一区二区| 视频在线观看一区| 亚洲高清日韩精品第一区| 一区二区三区观看| 国模极品一区二区三区| 日韩精品一区二区三区国语自制 | 91福利国产在线观看一区二区| 亚洲AV无码一区二区一二区| 日韩人妻无码一区二区三区久久99 | 久久久人妻精品无码一区| 丰满爆乳无码一区二区三区|