暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

开源网关apisix源码阅读和最佳实践

放马 2020-06-10
2881


大家应该都接手过这种项目,前人找一个开源软件改一改,发上线。

 

我这里便曾经遇到过类似的问题。


随着需求的增加,各种维护人员东改改西改改,原来的开源项目被改的面目全非,再也无法和上游合并。

 

甚至TLS协议栈被改掉,不再兼容HTTPS协议。

 

然后有些人基于这个代码又拖出来一份新代码,实现自己的功能。

 


核心代码被魔改,各种变量删的删,想法是好的,“方便维护”,“方便开源”,“方便KPI”,结果是睿智的。


这几个软件再也无法合并到上游,甚至彼此之间互相独立,成了三个仓库、五个分支、八个应用,更别说跟着开源项目一起更新。

 

最后这些项目因为都属于流量相关,最终流转到我这里。

 

我第一次理清这些关系的时候,高兴的一夜没睡觉。


本次apisix网关源码阅读和整理,便是为了摸透整个系统,归纳出最佳实践方案,避免自己像前人一样到处魔改,导致整个系统最终很难维护。






apisix选型



选择apisix的原因很简单,apisix解决了nginx两个最大的问题:“动态路由”、“动态证书”,大大方便了nginx网关的维护工作。同时得益于nginx+luajit,apisix的性能还算不错。


除此之外,apisix提供了相对比较完善的插件模式,方便我们进行插件的二次开发,在接下来的文章中我们会详细介绍apisix的源码和二次开发。






apisix实现方案


Tengine/Openresty的nginx-lua模块在nginx中预设了lua埋点,nginx在执行对应的模块时,会先调用对应的lua埋点,然后继续向下执行。


Apisix 则是基于这些埋点,做到了动态 host、动态证书等功能。


对于配置变更,则是监听etcd,使用etcd实现配置管理和配置变更通知,启动的时候设置定时器回调,使用轮询监听(3s超时并启动下一次监听)来进行变更监听。


再加上 nginx 本身的高性能、热加载能力,和 luajit 本身的高性能,apisix 性能不会比 nginx 转发性能弱太多。


如果有一张图解释清楚apisix的话,那么应该是:







而apisix并没有使用全部的十一个埋点,而是选择了其中的八个埋点,分为两个部分:初始化部分和请求部分。



证书处理,ssl_certificate_by_lua_block,执行apisix.http_ssl_phase()

未使用埋点:set_by_lua、rewrite_by_lua,其中rewrite功能由access_by_lua_block块代为实现。

请求处理,access_by_lua_block,执行apisix.http_access_phase()

请求代理:balancer_by_lua_block,执行apisix.http_balancer_phase()

请求返回头过滤:header_filter_by_lua_block,执行apisix.http_header_filter_phase(),调用header_filter插件。

请求返回body过滤:body_filter_by_lua_block,执行apisix.http_body_filter_phase(),调用body_filter插件。

日志模块:log_by_lua_block执行apisix.http_log_phase()







apisix概念


apisix共有三个概念:route、service、upstream。


Route 就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的 插件,并把请求转发给到指定 Upstream或者Service。


一个路由建议指向一个或者多个域名+uri。


Service 是某类 API 的抽象(也可以理解为一组 Route 的抽象)。它通常与上游服务抽象是一一对应的,Route 与 Service 之间,通常是 N:1 的关系。


Upstream 是虚拟主机抽象,对给定的多个服务节点按照配置规则进行负载均衡。虽然Upstream 的地址信息可以直接绑定到 Route(或 Service) 上,但是仍然建议单独配置Upstream,并通过“引用”的方式避免重复。





apisix初始化


apisix的初始化流程分为两步:http_init和http_init_worker,http_init执行一次,而http_init_worker则是每个worker执行一次。





    http_init 初始化函数
    > set: jit_stack_size jit.opt 设置一些luajit的参数
    > randomseed 初始化随机种子
    > parse_args set DNS 设置DNS
      > core.id.init() 初始化随机ID,用于后期的健康检查、状态上报。
      http_init_worker初始化worker,每个worker执行一次。
      > apisix.discovery.init_worker > 初始化服务发现中心(仅支持eureka)
      > apisix.balancer.init_worker() > 监听 etcd upstreams
      > apisix.admin.init.init_worker() > 初始化 apisix/admin 管理路由
      > router.http_init_worker()
      > apisix.http.router.radixtree_uri.init_worker() > 开始监听 etcd routes 并生成 http 路由
      > apisix.http.router.radixtree_sni.init_worker() > 开始监听 etcd ssl 监听证书
      > 监听 etcd global_rules 全局插件
      > apisix.http.service.init_worker() > 开始监听 etcd services 监听服务
      > plugin.init_worker() 插件加载逻辑
      > load plugin 加载插件
      > foreach load_plugin()
      > _plugin_.init() 调用插件init方法
      > sort by priority 对所有(包括未加载)的插件按照优先级排序
      > load stream plugin 加载流处理插件
      > default only mqtt 目前仅支持mqtt
      > apisix.consumer.init_worker() > 开始监听 etcd consumers
      > apisix.core.config_yaml.init_worker() 如果注册中心使用yaml的话,加载配置文件。
      > apisix.debug.init_worker() 调试相关方法
      > 初始化dns缓存




      apisix请求处理


      在apisix中,一个完整的请求生命周期,从前到后包含六个部分,请求如图所示:




      apisix的大部分逻辑还是在配置变更方面,用于处理请求的代码反而不是很多,整体处理流程如下:


        http_ssl_phase
        > router.router_ssl.match_and_set(api_ctx)
        > 设置上下文
        > radixtree_router:dispatch获取证书,如果获取不到则返回failed to fetch SNI
        > set_pem_ssl_key 清除默认证书,并使用etcd中的证书替换


        http_access_phase
        > 调用全局rewrite插件
        > 调用全局access插件
        > radixtree_uri match域名、路由匹配
        > 如果是grpc则转发到grpc逻辑处理
          > 获取service并合并service配置
        > 解析后端域名
        > 调用rewrite插件
          > 合并consumer配置,做身份认证
          > 调用access插件


        http_balancer_phase
        > 如果有balancer插件,则调用balancer插件。
        > 否则调用默认的balancer插件。


        common_phase(header_filter)
        > 调用header_filter插件


        common_phase(body_filter)
        > 调用body_filter插件


        http_log_phase
        > 调用log插件
          > 清理tablepool





        apisix运维方案


        在弄清了apisix的架构和使用方法之后,运维方案便比较简单了:


        1. 目录方案:apisix不强行依赖任何文件目录,可以随意定制,只需要系统PATH中存在openresty。

        2. 启动方案:openresty -p $openresty_dir -c $openresty_dir/conf/nginx.conf

        3. 停止方案:openresty -p $openresty_dir -c $openresty_dir/conf/nginx.conf -s stop

        4. reload方案:openresty -p $openresty_dir -c $openresty_dir/conf/nginx.conf -s reload

        5. 扩容方案:LVS后面增加节点。




        apisix管理方案:


        有两种配置管理方案:

        1. apisix自身实现了配置管理的部分接口,可以通过调用接口进行配置管理。apisix同时提供了简易的控制台(vuejs),可以基于此二次开发。

        2. 直接操作etcd。


        apisix 管理接口权限校验方案:

        目前apisix管理api提供了两种鉴权方案,

        1. ip白名单。

        2. 写在配置中的token。

        备注:目前apisix还没有提供动态可配token的方案,所以前期需要ip白名单+token一起用。




        apisix证书管理方案:


        证书更换是我以前面临过的问题之一。

        一般半年或者一年一换,所有涉及到证书的系统重启更新证书,是一件体力活。


        而apisix的证书存储在etcd,可以通过接口或者api动态配置,在ssl_certificate_by_lua_block阶段替换证书。

         

          http_ssl_phase
          > core.tablepool.fetch("api_ctx")
          > router.router_ssl.match_and_set(api_ctx)
          > create_router
          > radixtree_router:dispatch 获取证书,如果获取不到则返回failed to fetch SNI
          > set_pem_ssl_key 清除默认证书,并使用etcd中的证书替换




          apisix与k8s集成方案:


          两种方式:

          1. 使用apisix提供的ingress-controller,绑定到service。

          2. 使用k8s提供的clusterip。

          3. upstream提供了k8s_deployment_info选项,但是目前还是TODO状态。

          备注:

          ingress controller 尚不完善,可能需要投入人力进一步开发。




          apisix日志收集方案:


          nginx日志:

          使用logkit,filebeat,fluentd或者logagent,都会提供对应的nginx日志收集工具收集nginx日志。


          其他日志:

          apisix也预设了kafka-logger、tcp-logger、http-logger、syslog插件,也可以自己开发插件,也可以自己开发。






          apisix 二次开发方案:


          apisix插件逻辑预设了如下埋点,都是一些通用插件逻辑,不再赘述:



            属性:
            attr:
            version
            priority
            name


            方法:
            init
            check_schema 检查配置json格式
            rewrite
            access
            balancer
            header_filter
            body_filter
            log


            我们知道,分布式追踪在网关上面的实现方式,用一句话描述的话,“在请求头中预埋trace id”。


            以自带的zipkin插件为例:


              rewrite 阶段进行trace id埋点
              function _M.rewrite(conf, ctx)
              .......
              ctx.opentracing_sample = tracer.sampler:sample()
              .......
              local request_span = tracer:start_span("apisix.request", {
              child_of = wire_context,
              start_timestamp = start_timestamp,
              tags = {
              component = "apisix",
              ["span.kind"] = "server",
              ["http.method"] = ctx.var.request_method,
                          ["http.url"] = ctx.var.request_uri,
              })
              .......
              end


              access阶段启动一个子span,用于统计access阶段的调用时长
              function _M.access(conf, ctx)
              ......
              opentracing.proxy_span = opentracing.request_span:start_child_span(
              "apisix.proxy", ctx.ACCESS_END_TIME)
              ......
              end


              log阶段作收尾和上报工作
              function _M.log(conf, ctx)
              ......
              local log_end_time = opentracing.tracer:time()
              opentracing.body_filter_span:finish(log_end_time)
              ......
              opentracing.request_span:set_tag("http.status_code", upstream_status)
              opentracing.proxy_span:finish(log_end_time)
              opentracing.request_span:finish(log_end_time)
              ......
              local ok, err = ngx.timer.at(0, report2endpoint, reporter)
              end


              所以如果我们想要实现对应的插件,只需要按照apisix插件的格式,实现对应的功能,并注册到config.yaml中。






              apisix的缺陷:


              apisix目前存在部分问题,根据我所看到的代码和实践经验,总结如下:



              可能存在问题的模块
              可能存在的问题
              心跳模块

              heartbeat默认每小时向https://www.iresty.com/apisix/heartbeat发送一次心跳信息,可能存在信息泄露的风险,需要手动关闭该模块。

              请求重试

              默认不会配置健康检查请求。


              请求失败,重试次数至少一次,默认重试次数和upstream中配置的vhost个数数相同,无法设置不重试。

              当负载均衡算法为roundrobin时,会将upstream中所有的上游重试一遍。

              当负载均衡算法为hash时,只会对当前的上游机器进行重试。

              限流插件

              全局限流插件依靠redis,一次请求对应三次redis操作。可能会导致redis流量过大,需要手动改成令牌桶等算法。








              apisix的性能测试


              本性能测试不算特别规范,尤其是当我使用压测机2测试的时候,家用交换机的延迟在0.5ms左右,比请求本身的时间还要高,所以测试数据仅供参考。


              测试方法:wrk -d 10 -c ${连接数} http://192.168.56.26:9080/hello

              测试参数:服务线程:1,转发线程:1。



              场景openresty提供服务使用openresty转发使用apisix转发(不开启插件)
              使用apisix转发(开启限流插件)
              本机施压/2连接数68461 0.03ms15895 0.12ms18938 0.1ms

              19058 0.1ms

              本机施压/16连接数 69565 0.22ms17459 0.92ms41859 0.48ms

              37906 0.49ms

              测试机2施压/2连接数2971 0.66ms2568 0.76ms 2084 / 0.94ms

              2189 / 0.90ms

              测试机2施压/16连接数17654 / 0.82ms13899 / 1.02ms16670 / 0.87ms

              16186 / 0.90ms


              没错,我个人测试的场景中,openresty提供服务+openresty转发,性能好像更差一点。





              apisix的用户


              我一向提倡“eat your own dog food”的开发模式,自己是自己的用户,自己是自己的开发。


              homelabbity.com的技术栈如下图所示:



              而在调研了apisix之后,homelabbity.com也很快迁移到了apisix,效果如图所示:



              jenkins本身依赖一些比较高级的nginx参数,因此不太好向apisix迁移,我正在寻找新的解决办法。


              这也引出了最后一个问题。





              当业务比较简单、使用nginx独有特性不太多的情况下,是很好向apisix做迁移的。


              但是当业务大量依赖nginx的某些功能,或者背有沉重的历史包袱的时候,迁移的成本比较高。插件可以写,功能可以做,但是梳理域名终归是个细致的体力活,一不小心就做错。


              这也就是目前迁移到apisix可能存在的最大问题。



              文章转载自放马,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论