APISIX(v1.5)启动过程源码分析

3,246

0x00 环境

我在macos上源码安装了apisix1.5版本,etcd和其他的服务用docker-compose部署的,apisix有docker部署的教程。环境部署在此不赘述。

apisix v1.5源码:github.com/apache/apis…

我在分析过程中所使用的调试代码:github.com/tzssangglas… 这部分代码把启动过程拆分为几个步骤,准备好调试环境,方便一步步debug,理清楚启动过程。

0x01 启动

apisix的安装环境位于/usr/local/Cellar/apisix/apache-apisix-1.5,启动命令是./bin/apisix restart

该启动命令对应的是/usr/local/Cellar/apisix/apache-apisix-1.5/bin/apisix脚本,该脚本首行代码

#!/usr/bin/env lua

在类unix 操作系统里,在脚本首行加上 #! (这叫作 shebang或者 pound bang),其后加上用于解释这个脚本的解释程序的绝对路径,但是很显然这个后面跟的不是绝对路径。

env是一个可执行命令,告诉操作系统在当前系统的环境变量$PATH下去寻找

#查看PATH环境变量
echo $PATH
#输出
/usr/local/opt/openssl@1.1/bin:/usr/local/Cellar/openresty@1.15.8.3/1.15.8.3/openresty/luajit/bin:/usr/local/Cellar/apisix/apache-apisix-1.5/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/Applications/Wireshark.app/Contents/MacOS

env lua : 指的是使用当前env环境内的配置的Path路径下的寻找名为lua的解释器(指令)

#查看lua指令位置
which lua
#输出
/usr/local/Cellar/apisix/apache-apisix-1.5

可以看到,lua所在位置在$PATH环境变量下。如果把这个路径下的lua解释器删掉,而且在其他路径下也没有lua解释器,那么启动apisix会报错

env: lua: No such file or directory

这种写法的有点主要是让这个脚本在不同系统,不同环境及不同用户权限下适用,只要解释器程序恰当地把自己的路径写入$PATH环境变量。

0x02 start

start指令对应的函数

function _M.start(...)
    -- check running
    local pid_path = apisix_home .. "/logs/nginx.pid"
    local pid, err = read_file(pid_path)  -- 读取/usr/local/apisix/logs/nginx.pid
    ……
end

上面代码涉及到的apisix_home在脚本其他地方已经定义好了,之所以把apisix_home变量单独剥离出来,是因为这个变量就是当前apisix的安装目录,要获取这个值比较繁琐,因为在不同的系统中,不同的安装方式,都会导致apisix的安装目录不可知,所以要抽出来,单独写一段代码确定这个值。

apisix_home: 确定apisix的安装目录
local apisix_home = "/usr/local/apisix"  --linux环境中默认的安装地址

/usr/local/apisix是linux环境默认的安装目录,但是不绝对,也可能被用户安装在其他地方,比如我本地安装就不在这个目录。

所以apisix对apisix_home做了处理,就是调用函数来执行pwd命令,获取脚本所在的绝对路径,过程如下

--我的启动命令是./bin/apisix start
--如果启动命令以'./'开头
if script_path:sub(1, 2) == './' then
    --调用系统的cmd命令,查看当前所在路径
    apisix_home = trim(excute_cmd("pwd"))  
    if not apisix_home then
        error("failed to fetch current path")
    end
    
    --判断是否安装在root跟路径下
    if string.match(apisix_home, '^/[root][^/]+') then
            is_root_path = true
    end
end

excute_cmd函数如下

-- 注意:`excute_cmd`返回值的末尾会有换行符,建议使用`trim`函数处理返回值。
local function excute_cmd(cmd)
    --使用io.popen函数来执行命令(和直接在命令行界面执行命令的结果上一样的,不过使用函数这种方式的结果保存在文件中),在这里执行的就是下面的pwd命令
    local t, err = io.popen(cmd)  --这里返回的t其实是一个文件描述符
    if not t then
        return nil, "failed to execute command: " .. cmd .. ", error info:" .. err
    end
  
    local data = t:read("*all")   --执行read命令读取文件
    t:close()
    return data   --在我的机器上,模拟apisix脚本所在位置,执行pwd命令,返回如下
  								--/usr/local/Cellar/apisix/apache-apisix-1.5/bin\n
                  --结尾的\n是换行符
end

用IDEA+EmmyLua插件搭了lua脚本调试环境,把excute_cmd函数扒出来调试,如下

io.popen()命令解释

  • 原型:io.popen ([prog [, mode]])
  • 解释:在额外的进程中启动程序prog,并返回用于prog的文件句柄。通俗的来说就是使用这个函数可以调用一个命令(程序),并且返回一个和这个程序相关的文件描述符,一般是这个被调用函数的输出结果,这个文件打开模式由参数mode确定,有取值"r""w"两种,分别表示以读、写方式打开,默认是以读的方式。

经过上面的处理,apisix_home已经是正确的apisix的安装目录了。我的环境中,apisix_home=/usr/local/Cellar/apisix/apache-apisix-1.5,继续看start函数。

read_file: 读取nginx.pid,判断apisix是都running
function _M.start(...)
    -- check running
    local pid_path = apisix_home .. "/logs/nginx.pid"
    local pid, err = read_file(pid_path)  -- 读取/usr/local/apisix/logs/nginx.pid
    --如果nginx的进程id存在
    if pid then
        local hd = io.popen("lsof -p " .. pid)
        local res = hd:read("*a")
        if res and res ~= "" then
            print("APISIX is running...")
            return nil
        end
    end
    
    --启动apisix
    init(...)
    --启动etcd
    init_etcd(...)

    local cmd = openresty_args
    -- print(cmd)
    os.execute(cmd)
end

nginx.pid其实在启动nginx 自动生成的,里面存放的是当前nginx 住进程的ID号,即pid。我本地apisix已经启动了,看一下nginx.pid

到第4行,其实就是找到apisix运行时目录。

这里用到了io.read()函数,传的参数有"*a""*all"两个,我debug并对比结果看了一下,这两个参数应该都是相同的意思,从其当前位置开始,读取整个文件。如果位于文件末尾,或者文件为空,则调用将返回一个空字符串。在文件结束时,它返回空字符串。

"*all"是lua5.1的写法,参考:www.lua.org/pil/21.1.ht…

"*a"是lua5.2的写法,参考:www.lua.org/manual/5.2/…

到第13行,就是检测nginx是否在running中,如果是,则打印日志并退出。

init: 启动apisix

start函数中显示调用了init函数,参数中的三个点(...)表示该函数可接受不同数量的实参。

local function init()
    --判断apisix的安装目录是否是根路径
    if is_root_path then
        print('Warning! Running apisix under /root is only suitable for development environments'
            .. ' and it is dangerous to do so. It is recommended to run APISIX in a directory other than /root.')
    end

    -- read_yaml_conf
    --读取配置文件
    local yaml_conf, err = read_yaml_conf()
    if not yaml_conf then
        error("failed to read local yaml config of apisix: " .. err)
    end
    -- print("etcd: ", yaml_conf.etcd.host)

    ……
end

先看read_yaml_conf()函数

local function read_yaml_conf()
    --调用profile模块
    local profile = require("apisix.core.profile")
    --给profile模块的元表apisix_home属性赋值,其实就是让profile模块知道apisix的安装路径
    profile.apisix_home = apisix_home .. "/"
    --profile模块yaml_path函数就是用 
    --apisix_home  .. "conf/" .. "config" .. ".yaml" 
    --进行拼接,在我的环境下,这个拼接结果就是一个字符串:
    --"/usr/local/Cellar/apisix/apache-apisix-1.5/conf/config.yaml"
    --即apisix的配置文件所在位置
    local local_conf_path = profile:yaml_path("config")
    --读取配置文件
    local ymal_conf, err = read_file(local_conf_path)
    if not ymal_conf then
        return nil, err
    end
    -- 解析tinyyaml模块来yaml配置文件
    return yaml.parse(ymal_conf)
end

这一段主要就是根据apisix的安装路径,拼接出配置文件config.yaml的绝对路径,然后进行io读取文件,交给tinyyaml模块来解析yaml配置,得到的是yaml配置被解析为table类型的值,yaml_conf的堆栈信息如下,世纪上就是把config.yaml转化成了table形式。

继续往下

local function init()
    --查看openresty编译信息
    local or_ver = excute_cmd("openresty -V 2>&1")
    local with_module_status = true
    --校验是否安装了http_stub_status_module模块
    if or_ver and not or_ver:find("http_stub_status_module", 1, true) then
        io.stderr:write("'http_stub_status_module' module is missing in ",
                        "your openresty, please check it out. Without this ",
                        "module, there will be fewer monitoring indicators.\n")
        with_module_status = false
    end
        ……
end

这一段相当于执行openresty -V 2>&1 | grep http_stub_status_module命令,主要是校验编译openresty在编译的时候是否编译了http_stub_status_module模块,没有编译的话,监控指标会变少。

继续,下面是准备系统上下文环境,这一段主要是对yaml_conf进行参数校验和转换,以及获取系统运行时环境相关的信息,最终获得一个完整的table形式的sys_conf

local function init()
    ……
    -- Using template.render
    --准备上下文信息
    local sys_conf = {
        lua_path = pkg_path_org,
        lua_cpath = pkg_cpath_org,
        --获取当前系统相关信息
        os_name = trim(excute_cmd("uname")),
        apisix_lua_home = apisix_home,
        with_module_status = with_module_status,
        error_log = {level = "warn"},
    }
    --一系列校验
    if not yaml_conf.apisix then
        error("failed to read `apisix` field from yaml file")
    end

    if not yaml_conf.nginx_config then
        error("failed to read `nginx_config` field from yaml file")
    end
    --校验是否是32位机器
    if is_32bit_arch() then
        --worker_rlimit_core: nginx的配置,设置每个worker最大能打开的核心文件数,用于在不重启主进程的情况下增加限制。
        sys_conf["worker_rlimit_core"] = "4G"
    else
        sys_conf["worker_rlimit_core"] = "16G"
    end
    --yaml_conf中的配置信息转移到sys_conf
    for k,v in pairs(yaml_conf.apisix) do
        sys_conf[k] = v
    end
    for k,v in pairs(yaml_conf.nginx_config) do
        sys_conf[k] = v
    end
    --参数校验&优化参数
    local wrn = sys_conf["worker_rlimit_nofile"]
    local wc = sys_conf["event"]["worker_connections"]
    if not wrn or wrn <= wc then
        -- ensure the number of fds is slightly larger than the number of conn
        sys_conf["worker_rlimit_nofile"] = wc + 128
    end
    --是否开启dev模式
    if(sys_conf["enable_dev_mode"] == true) then
        --开启dev模式的话,设置worker数量
        sys_conf["worker_processes"] = 1
        sys_conf["enable_reuseport"] = false
    else
        sys_conf["worker_processes"] = "auto"
    end
    --是否配置外置dns解析
    local dns_resolver = sys_conf["dns_resolver"]
    if not dns_resolver or #dns_resolver == 0 then
        --如果没有配置dns解析,则使用默认的解析
        local dns_addrs, err = local_dns_resolver("/etc/resolv.conf")
        if not dns_addrs then
            error("failed to import local DNS: " .. err)
        end

        if #dns_addrs == 0 then
            error("local DNS is empty")
        end
        sys_conf["dns_resolver"] = dns_addrs
    end
    --到这里,sys_conf信息基本准备完毕
    ……
end

这一部分虽然看着代码比较多,但是并不复杂,一部分是参数校验,一部分是获取运行时的服务器信息。

基本都是围绕着准备sys_conf来进行。

yaml_conf来自于用户配置的信息,是sys_conf的一部分,根据用户的配置参数,比如是否开启dev模式等,来决定sys_conf中一些参数的配置。

所以这里主要是根据yaml_conf,操作系统环境,openresty编译信息,apisix的安装环境,lua的PATH环境变量等参数,组装sys_conf,而sys_conf会用来定义nginx的行为的,即转换成nginx.conf配置文件。

最终完成的sys_conf堆栈信息如下

go on,下面就是把sys_conf转化成nginx.conf配置文件了,用到了一个函数库lua-resty-template,template用法参考github.com/bungle/lua-…

 local function init()
    ……
    --获取模板渲染引擎,ngx_tpl是在代码中定义的nginx-template模板,太长了,在这里就不展现了
    local conf_render = template.compile(ngx_tpl)
    --执行模板渲染,其实就是把sys_conf中的参数渲染进ngxconf中
    local ngxconf = conf_render(sys_conf)

    --把ngxconf配置写入文件,即nginx.conf
    local ok, err = write_file(apisix_home .. "/conf/nginx.conf", ngxconf)
    if not ok then
        error("failed to update nginx.conf: " .. err)
    end

    --获取openresty版本号,调用的是local 函数,其实就是执行cmd命令
    local op_ver = get_openresty_version()
    --在我的环境中,op_ver = 1.15.8.3
    if op_ver == nil then
        io.stderr:write("can not find openresty\n")
        return
    end

    local need_ver = "1.15.8"
    --校验openresty版本号,校验的方式就是把op_ver和need_ver用 . 分割成数组,然后挨个比较大小
    if not check_or_version(op_ver, need_ver) then
        io.stderr:write("openresty version must >=", need_ver, " current ", op_ver, "\n")
        return
    end
end

这一段,包括上一段,用到了一些local函数,get_openresty_version(),check_or_version之类的,其实和前面介绍过的那些函数类似,都是io和cmd操作。

ngxconf是模板引擎渲染出来的结果,是一个string类型的值,里面的内容如下

然后就是把内容输出到文件,最终生成的nginx.conf文件如下

# Configuration File - Nginx Server Configs
# This is a read-only file, do not try to modify it.

master_process on;

worker_processes auto;

error_log logs/error.log warn;
pid logs/nginx.pid;

worker_rlimit_nofile 20480;

events {
    accept_mutex off;
    worker_connections 10620;
}

worker_rlimit_core  16G;

worker_shutdown_timeout 240s;

env APISIX_PROFILE;


http {
    lua_package_path  "$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;/usr/local/Cellar/apisix/apache-apisix-1.5/?.lua;/usr/local/Cellar/apisix/apache-apisix-1.5/?/init.lua;;./?.lua;/usr/local/Cellar/openresty@1.15.8.3/1.15.8.3/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/Cellar/openresty@1.15.8.3/1.15.8.3/openresty/luajit/share/lua/5.1/?.lua;/usr/local/Cellar/openresty@1.15.8.3/1.15.8.3/openresty/luajit/share/lua/5.1/?/init.lua;/Users/tuzhengsong/Library/Application Support/JetBrains/IntelliJIdea2020.2/plugins/intellij-emmylua/classes/debugger/mobdebug/?.lua;/Users/tuzhengsong/IdeaProjects/apisix-learning/src/?.lua;/Users/tuzhengsong/IdeaProjects/apisix-learning/?.lua;;/usr/local/Cellar/apisix/apache-apisix-1.5/deps/share/lua/5.1/?.lua;/usr/local/Cellar/apisix/apache-apisix-1.5/deps/share/lua/5.1/?/?.lua;/usr/local/Cellar/apisix/apache-apisix-1.5/deps/share/lua/5.1/?.lua;";
    lua_package_cpath "$prefix/deps/lib64/lua/5.1/?.so;$prefix/deps/lib/lua/5.1/?.so;;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/Cellar/openresty@1.15.8.3/1.15.8.3/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;";

    lua_shared_dict plugin-limit-req     10m;
    lua_shared_dict plugin-limit-count   10m;
    lua_shared_dict prometheus-metrics   10m;
    lua_shared_dict plugin-limit-conn    10m;
    lua_shared_dict upstream-healthcheck 10m;
    lua_shared_dict worker-events        10m;
    lua_shared_dict lrucache-lock        10m;
    lua_shared_dict skywalking-tracing-buffer    100m;


    # for openid-connect plugin
    lua_shared_dict discovery             1m; # cache for discovery metadata documents
    lua_shared_dict jwks                  1m; # cache for JWKs
    lua_shared_dict introspection        10m; # cache for JWT verification results

    # for custom shared dict

    # for proxy cache
    proxy_cache_path /tmp/disk_cache_one levels=1:2 keys_zone=disk_cache_one:50m inactive=1d max_size=1G;

    # for proxy cache
    map $upstream_cache_zone $upstream_cache_zone_info {
        disk_cache_one /tmp/disk_cache_one,1:2;
    }

    lua_ssl_verify_depth 5;
    ssl_session_timeout 86400;

    underscores_in_headers on;

    lua_socket_log_errors off;

    resolver 192.168.1.1 192.168.0.1 valid=30;
    resolver_timeout 5;

    lua_http10_buffering off;

    lua_regex_match_limit 100000;
    lua_regex_cache_max_entries 8192;

    log_format main '$remote_addr - $remote_user [$time_local] $http_host "$request" $status $body_bytes_sent $request_time "$http_referer" "$http_user_agent" $upstream_addr $upstream_status $upstream_response_time';

    access_log logs/access.log main buffer=16384 flush=3;
    open_file_cache  max=1000 inactive=60;
    client_max_body_size 0;
    keepalive_timeout 60s;
    client_header_timeout 60s;
    client_body_timeout 60s;
    send_timeout 10s;

    server_tokens off;
    more_set_headers 'Server: APISIX web server';

    include mime.types;
    charset utf-8;

    real_ip_header X-Real-IP;

    set_real_ip_from 127.0.0.1;
    set_real_ip_from unix:;

    upstream apisix_backend {
        server 0.0.0.1;
        balancer_by_lua_block {
            apisix.http_balancer_phase()
        }

        keepalive 320;
    }

    init_by_lua_block {
        require "resty.core"
        apisix = require("apisix")

        local dns_resolver = { "192.168.1.1", "192.168.0.1", }
        local args = {
            dns_resolver = dns_resolver,
        }
        apisix.http_init(args)
    }

    init_worker_by_lua_block {
        apisix.http_init_worker()
    }


    server {
        listen 9080 reuseport;
        listen 9443 ssl http2 reuseport;


        listen [::]:9080 reuseport;
        listen [::]:9443 ssl http2 reuseport;

        ssl_certificate      cert/apisix.crt;
        ssl_certificate_key  cert/apisix.key;
        ssl_session_cache    shared:SSL:20m;
        ssl_session_timeout 10m;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers on;

        location = /apisix/nginx_status {
            allow 127.0.0.0/24;
            deny all;
            access_log off;
            stub_status;
        }

        location /apisix/admin {
                allow 127.0.0.0/24;
                deny all;

            content_by_lua_block {
                apisix.http_admin()
            }
        }

        location /apisix/dashboard {
                allow 127.0.0.0/24;
                deny all;

            alias dashboard/;

            try_files $uri $uri/index.html /index.html =404;
        }

        ssl_certificate_by_lua_block {
            apisix.http_ssl_phase()
        }

        location / {
            set $upstream_mirror_host        '';
            set $upstream_scheme             'http';
            set $upstream_host               $host;
            set $upstream_upgrade            '';
            set $upstream_connection         '';
            set $upstream_uri                '';

            access_by_lua_block {
                apisix.http_access_phase()
            }

            proxy_http_version 1.1;
            proxy_set_header   Host              $upstream_host;
            proxy_set_header   Upgrade           $upstream_upgrade;
            proxy_set_header   Connection        $upstream_connection;
            proxy_set_header   X-Real-IP         $remote_addr;
            proxy_pass_header  Server;
            proxy_pass_header  Date;

            ### the following x-forwarded-* headers is to send to upstream server

            set $var_x_forwarded_for        $remote_addr;
            set $var_x_forwarded_proto      $scheme;
            set $var_x_forwarded_host       $host;
            set $var_x_forwarded_port       $server_port;

            if ($http_x_forwarded_for != "") {
                set $var_x_forwarded_for "${http_x_forwarded_for}, ${realip_remote_addr}";
            }
            if ($http_x_forwarded_proto != "") {
                set $var_x_forwarded_proto $http_x_forwarded_proto;
            }
            if ($http_x_forwarded_host != "") {
                set $var_x_forwarded_host $http_x_forwarded_host;
            }
            if ($http_x_forwarded_port != "") {
                set $var_x_forwarded_port $http_x_forwarded_port;
            }

            proxy_set_header   X-Forwarded-For      $var_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto    $var_x_forwarded_proto;
            proxy_set_header   X-Forwarded-Host     $var_x_forwarded_host;
            proxy_set_header   X-Forwarded-Port     $var_x_forwarded_port;

            ###  the following configuration is to cache response content from upstream server

            set $upstream_cache_zone            off;
            set $upstream_cache_key             '';
            set $upstream_cache_bypass          '';
            set $upstream_no_cache              '';
            set $upstream_hdr_expires           '';
            set $upstream_hdr_cache_control     '';

            proxy_cache                         $upstream_cache_zone;
            proxy_cache_valid                   any 10s;
            proxy_cache_min_uses                1;
            proxy_cache_methods                 GET HEAD;
            proxy_cache_lock_timeout            5s;
            proxy_cache_use_stale               off;
            proxy_cache_key                     $upstream_cache_key;
            proxy_no_cache                      $upstream_no_cache;
            proxy_cache_bypass                  $upstream_cache_bypass;

            proxy_hide_header                   Cache-Control;
            proxy_hide_header                   Expires;
            add_header      Cache-Control       $upstream_hdr_cache_control;
            add_header      Expires             $upstream_hdr_expires;
            add_header      Apisix-Cache-Status $upstream_cache_status always;

            proxy_pass      $upstream_scheme://apisix_backend$upstream_uri;
            mirror          /proxy_mirror;

            header_filter_by_lua_block {
                apisix.http_header_filter_phase()
            }

            body_filter_by_lua_block {
                apisix.http_body_filter_phase()
            }

            log_by_lua_block {
                apisix.http_log_phase()
            }
        }

        location @grpc_pass {

            access_by_lua_block {
                apisix.grpc_access_phase()
            }

            grpc_set_header   Content-Type application/grpc;
            grpc_socket_keepalive on;
            grpc_pass         grpc://apisix_backend;

            header_filter_by_lua_block {
                apisix.http_header_filter_phase()
            }

            body_filter_by_lua_block {
                apisix.http_body_filter_phase()
            }

            log_by_lua_block {
                apisix.http_log_phase()
            }
        }

        location = /proxy_mirror {
            internal;

            if ($upstream_mirror_host = "") {
                return 200;
            }

            proxy_pass $upstream_mirror_host$request_uri;
        }
    }
}

到这里,启动过程并没有结束,只是生成了需要的nginx.conf配置文件。

init_etcd: 启动etcd

下面就是启动etcd了,这一段做的主要是读取yaml_conf中关于etcd相关的设置。

local function init_etcd(show_output)
    -- 读取yaml_conf
    local yaml_conf, err = read_yaml_conf()
    --一系列的参数校验
    if not yaml_conf then
        error("failed to read local yaml config of apisix: " .. err)
    end

    if not yaml_conf.apisix then
        error("failed to read `apisix` field from yaml file when init etcd")
    end

    if yaml_conf.apisix.config_center ~= "etcd" then
        return true
    end

    if not yaml_conf.etcd then
        error("failed to read `etcd` field from yaml file when init etcd")
    end
    --最终拿到根etcd相关的参数
    local etcd_conf = yaml_conf.etcd
    ……
end

etcd_conf配置堆栈信息如下

继续

local function init_etcd(show_output)
    ……
    local timeout = etcd_conf.timeout or 3
    local uri
    --将旧的单个etcd配置转换为多个etcd配置
    if type(yaml_conf.etcd.host) == "string" then
        yaml_conf.etcd.host = {yaml_conf.etcd.host}
    end
    --获取配置的etcd集群的多个地址
    local host_count = #(yaml_conf.etcd.host)

    -- 检查用户是否已启用etcd v2协议
    for index, host in ipairs(yaml_conf.etcd.host) do
        uri = host .. "/v2/keys"
        --这里就是拼接cmd命令了,在我的环境中,拼接出来的命令就是
        --curl -i -m 60 -o /dev/null -s -w %{http_code} http://127.0.0.1:2379/v2/keys
        --这个命令相当于获取curl访问结果的http状态码
        local cmd = "curl -i -m ".. timeout * 2 .. " -o /dev/null -s -w %{http_code} " .. uri  
        --执行命令
        local res = excute_cmd(cmd)
        --校验返回结果
        if res == "404" then
            --io.stderr:标准错误输出
            io.stderr:write(string.format("failed: please make sure that you have enabled the v2 protocol of etcd on %s.\n", host))
            return
        end
    end
    ……
end

这里就是根据配置的etcd集群的的host信息(可能有多个),用curl命令去访问,获取访问结果的状态码,校验用户是否开启了etcd的v2协议。

相当于执行curl -i -m 60 -o /dev/null -s -w %{http_code} http://127.0.0.1:2379/v2/keys

效果如下

继续

local function init_etcd(show_output)
    ……
    local etcd_ok = false
    --遍历etcd集群的主机地址
    for index, host in ipairs(yaml_conf.etcd.host) do

        local is_success = true
        --host --> http://127.0.0.1:2379
        --etcd_conf.prefix --> /apisix
        uri = host .. "/v2/keys" .. (etcd_conf.prefix or "")
        --准备在etcd中创建一些目录
        for _, dir_name in ipairs({"/routes", "/upstreams", "/services",
                                   "/plugins", "/consumers", "/node_status",
                                   "/ssl", "/global_rules", "/stream_routes",
                                   "/proto"}) do
            --拼接cmd命令
            local cmd = "curl " .. uri .. dir_name
                    .. "?prev_exist=false -X PUT -d dir=true "
                    .. "--connect-timeout " .. timeout
                    .. " --max-time " .. timeout * 2 .. " --retry 1 2>&1"
            --cmd示例: curl http://127.0.0.1:2379/v2/keys/apisix/routes?prev_exist=false -X PUT -d dir=true --connect-timeout 30 --max-time 60 --retry 1 2>&1
            local res = excute_cmd(cmd)
            --curl命令的返回结果中进行字符串匹配,找到有没有index或者createdIndex的关键词
            --这个判断条件的意思是:在返回结果中即没有匹配上createdIndex(创建成功时返回的res中含有的关键词),也没有匹配上index(重复创建时返回的关键词)
            if not res:find("index", 1, true)
                    and not res:find("createdIndex", 1, true) then
                --如果进入这里,说明本次循环(目录,routers之类的)中,当前要创建的资源实际上无法创建了,因为这个返回的结果既不是创建成功的结果,也不是重复创建的提示,那么这个资源无法创建了
                is_success = false
                --如果当前已经遍历到了配置的最后一个etcd host地址,那么直接报错,因为没有机会再次在其他host上创建这个资源了
                if (index == host_count) then
                    error(cmd .. "\n" .. res)
                end
                --如果是否的话,则退出当前循环,继续创建目录中的其他资源
                break
            end
            --是否输出cmd和res
            if show_output then
                print(cmd)
                print(res)
            end
        end
        --如果在一个host上创建成功,则退出循环(host),标识etcd_ok资源初始化成功
        if is_success then
            etcd_ok = true
            break
        end
    end
    
    --虽然可能会配置了etcd集群的多个host地址,但是只要有一个执行成功了初始化,就算完成了
    if not etcd_ok then
        error("none of the configured etcd works well")
    end
end

cmd命令拼接出来是:

curl http://127.0.0.1:2379/v2/keys/apisix/routes?prev_exist=false -X PUT -d dir=true --connect-timeout 30 --max-time 60 --retry 1 2>&1

这条命令里面如下参数是etcd v2 api接收的命令

  • prev_exist:检查键是否存在:如果prev_exist为真,则为更新请求;如果prev_exist为false,则它是一个创建请求
  • dir=true:创建目录

cmd的命令表述起来就是,在api上创建目录资源,创建成功后,etcd中相应的配置如下

cmd执行效果如下 创建成功时返回

{"action":"set","node":{"key":"/apiseven/routes","dir":true,"modifiedIndex":91,"createdIndex":91}}

如果相同的值已经创建过,则返回

{"errorCode":102,"message":"Not a file","cause":"/apiseven/upstreams","index":89}

所以在校验cmd执行结果的时候,会匹配indexcreatedIndex,只要匹配上其中一个,都说明这个目录创建成功了。

这个过程中的双层循环逻辑稍微有点绕,表述起来就是,要确保所有的目录资源,在配置的etcd集群中创建成功,尽管kennel不是在同一个host上创建的,但是只要都创建成功了,那么etcd会保证最终一致性。

至此,apisix启动过程中两个重要的环节

  • 创建nginx.conf
  • 初始化etcd的目录资源

已经完成。

openresty_args: 启动openresty

start函数最后一部分如下:

local openresty_args = [[openresty  -p ]] .. apisix_home .. [[ -c ]]
                       .. apisix_home .. [[/conf/nginx.conf]]

function _M.start(...)

    local cmd = openresty_args
    -- print(cmd)
    os.execute(cmd)
end

其实就是启动openresty了,apisix是基于openresy构建起来的一个应用

cmd命令拼接如下:

openresty  -p /usr/local/Cellar/apisix/apache-apisix-1.5 -c /usr/local/Cellar/apisix/apache-apisix-1.5/conf/nginx.conf
  • -p 指定项目目录
  • -c 指定配置文件

0x03 end

如有谬误,欢迎指出,共同探讨学习。