使用openresty+ xip 服务暴露k8s 部署的spring cloud 服务为外部可访问的地址

对于k8s外部服务暴露的方法是很多的(ingress,nodeport,直接通过api server 访问)个有利弊

问题

我们需要一个统一的入口方便访问spring cloud 部署的pod 服务,一般大家想到的是gateway
gateway 的确很不错,但是需要对于部署的每个pod 进行灵活的访问就不是很方便了

权衡

  • ingress 模式
    暴露的ingress太多了,需要手动处理的也太多了(不是很灵活)
  • nodeport 模式
    同ingress 一样,我们需要处理很多的nodeport ,同样需要管理端口
  • 正向代理
    我们需要进行配置(每个开发者),而且容易出现问题

思路

我们肯定是需要一个外部的入口,当时因为容器都是外部ip,所以我们需要一个方便的代理(支持灵活的策略)
单独基于ip的模式,肯定不方便,nginx 的虚拟主机就是一个不错的选择,所以我们需要一个灵活的域名处理机制
泛域名很好,但是因为特殊性(我们可能很难或者不是很方便提供类似的服务),xip 服务是一个不错的选择
我们可以基于xip 提供<servicename>.<ip>.xipdoman 格式的服务,servicename 就是spring cloud 在k8s中部署
的pod 名称,但是因为我们需要动态代理的是ip,所以我们需要获取ip地址(可以通过注册中心api),实际上整个
过程可以基于其他语言进行数据的获取,但是为了方便,直接使用openresty 处理各种api 数据的处理,对于动态代理
我们可以在set 阶段处理,但是有一个问题就是set 阶段不同使用cosocket,所以为了方便我们可以通过timer 绕过
简单的方法就是在init_worker 阶段同时我们通过shared 进行数据共享,方便数据的查询处理

解决方法

  • 参考图

使用openresty+ xip 服务暴露k8s 部署的spring cloud 服务为外部可访问的地址_分享

  • 简单说明
    整体处理就是基于上边思路的,只是我们的openresty服务部署在k8s中(主要是网络打通),同时为了方便域名访问我们还是部署了一个ingress
    (基于nginx)
  • 一些参考技术
    处理列表的我们使用了一个模版引擎lua-resty-template 实际上可以不用,主要是比较懒,用模版可以省好多时间,xip 服务使用了一个开源的实现
    https://github.com/peterhellberg/xip.name , 具体rpm 包参考https://github.com/rongfengliang/xip-rpm, 注册中心api 调用使用了lua-resty-http
  • 参考代码
    nginx 配置
 
worker_processes  1;
user root;  
events {
    worker_connections  1024;
}
env REGISTRY_HOST;
env REGISTRY_PORT;
env XIP_DOMAIN;
env XIP_IP;
env WEB_DOMAIN;
http {
    include       mime.types;
    default_type  application/octet-stream;
    gzip  on;
    resolver <dnsserver>;
    lua_shared_dict servicelist 100m;
    lua_code_cache off;
    real_ip_header     X-Forwarded-For;
    lua_package_path '/opt/app/?.lua;;';
    #  fetch list
    init_worker_by_lua_block {
         local delay = 10  -- in seconds
         local log = ngx.log
         local ERR = ngx.ERR
         local check
         check = function(premature)
             if not premature then
                local eurake_services = require("ds-api/services")
                local cjson = require("cjson.safe")
                log(ERR, "invoke serrvice list")
                local sharedservicelist = ngx.shared.servicelist;
                local servicelist  = eurake_services.servicelist()
                sharedservicelist:set("servicelist", cjson.encode(servicelist))
             end
         end
         if 0 == ngx.worker.id() then
             local ok, err = ngx.timer.every(delay, check)
             if not ok then
                 log(ERR, "failed to create timer: ", err)
                 return
             end
         end
      }
    server {
       listen 80;
       charset utf-8;
       default_type text/html;
       server_name *.xipservice;
       location / {
            add_header Access-Control-Allow-Origin "*";
            add_header Access-Control-Allow-Credentials 'true';
            add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
            add_header Access-Control-Allow-Headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';            
            if ($request_method = OPTIONS){
                return 200;
            }
            set_by_lua_block $my_host {
              local ngx_re = require("ngx.re")
              local proxy_server_host = ngx.var.host
              local servicelist = require("ds-api/services")
              ngx.log(ngx.ERR,"proxy_server_host  "..proxy_server_host)
              local from,to = string.find(proxy_server_host,[[xipip]])
              local result  = string.sub(proxy_server_host,1,from-2)
              local service_name = result
              ngx.log(ngx.ERR,"service_name  "..service_name)
              local server_address = servicelist.fetchinstance_backendip_cache(service_name) or "127.0.0.1"
              ngx.log(ngx.ERR,"server_address:"..server_address)
              return server_address
            }
            proxy_set_header Host $host;
            proxy_pass http://$my_host;
       }
    }    
    server {
        listen       80;
        server_name  <webpageforview>
        charset utf-8;
        default_type text/html;
        set $template_root /usr/local/openresty/nginx/html/templates;
        location / {
          root html;
          content_by_lua_block {
            local template = require "resty.template"
            local servicelist = require("ds-api/services").servicelist();
            template.render("index.html", servicelist)
          }
        }
        location /api/services/endpoint {
           content_by_lua_block {
                local cjson = require("cjson.safe")
                local eurake_services = require("ds-api/services"); 
                local servicename = ngx.req.get_uri_args()["servicename"]
                local server_address = eurake_services.fetchinstance_backendip(servicename)
                ngx.say(server_address)
            }
        }
        location /api/services/endpointcache {
           content_by_lua_block {
                local cjson = require("cjson.safe")
                local eurake_services = require("ds-api/services"); 
                local servicename = ngx.req.get_uri_args()["servicename"]
                local server_address = eurake_services.fetchinstance_backendip_cache(servicename)
                ngx.say(server_address)
            }
        }
        location /api/services {
             content_by_lua_block {
               local cjson = require("cjson.safe")
               local servicelist = require("ds-api/services").servicelist();
               ngx.say(cjson.encode(servicelist))
            }
        }
        location /api/services/ips {
             content_by_lua_block {
              local cjson = require("cjson.safe")
              local servicename = ngx.req.get_uri_args()["servicename"]
              local servicelist = require("ds-api/services").serviceips(servicename);
              ngx.say(cjson.encode(servicelist))
            }
        }       
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

注册中心服务获取

-- get service list
local http = require "resty.http"
local json = require "cjson.safe"
local utils = require("ds-api/utils")
local registryuri = os.getenv("REGISTRY_HOST");
local registryport = os.getenv("REGISTRY_PORT");
local xipdomainname = os.getenv("XIP_DOMAIN")
-- get service list
local function get_service_lists()
    ngx.log(ngx.ERR, "registryuri: ",registryuri)
    ngx.log(ngx.ERR, "registryport: ",registryport)
    local httpc = http:new()
    --connect_timeout, send_timeout, read_timeout (in ms)
    httpc:set_timeouts(3000, 10000, 10000)
    local params = utils.build_eureka_apps()
    local ok, err = httpc:connect(registryuri, registryport)
    if err ~= nil then
        ngx.log(ngx.ERR, "failed to connect eureka server: ",err)
        return nil, err
    end
    local res, err = httpc:request(params)
    if err ~= nil then
        ngx.log(ngx.ERR, "failed to request eureka service list: ", err)
        return nil, err
    end
    if res.status ~= 200 then
        return nil, "bad response code: " .. res.status
    end
    local body = res:read_body()
    local service = utils.parse_servicelist(body)
    local ok, err = httpc:set_keepalive(http_max_idle_timeout, http_pool_size)
    --local ok, err = httpc:set_keepalive()
    if err ~= nil then
        ngx.log(ngx.ERR, "eureka: failed to set keepalive for http client: ", err)
    end
    -- return ngx.say(json.encode(service))
    return service
end
local function get_service_ips(servicename)
    ngx.log(ngx.ERR, "registryuri: ",registryuri)
    ngx.log(ngx.ERR, "registryport: ",registryport)
    local httpc = http:new()
    --connect_timeout, send_timeout, read_timeout (in ms)
    httpc:set_timeouts(3000, 10000, 10000)
    local params = utils.build_app_upips(servicename)
    ngx.log(ngx.ERR, "servicename: ",servicename)
    local ok, err = httpc:connect(registryuri, registryport)
    if err ~= nil then
        ngx.log(ngx.ERR, "failed to connect eureka server: ",err)
        return nil, err
    end
    local res, err = httpc:request(params)
    if err ~= nil then
        ngx.log(ngx.ERR, "failed to request eureka service list: ", err)
        return nil, err
    end
    if res.status ~= 200 then
        return nil, "bad response code: " .. res.status
    end
    local body = res:read_body()
    local service, err = utils.parse_service(body)
    if err ~= nil then
        ngx.log(ngx.ERR, "eureka : failed to parse eureka service response: ", s_ip, ":", s_port, " ", err)
        return nil, err
    end
    local ok, err = httpc:set_keepalive(http_max_idle_timeout, http_pool_size)
    --local ok, err = httpc:set_keepalive()
    if err ~= nil then
        ngx.log(ngx.ERR, "eureka: failed to set keepalive for http client: ", err)
    end
    -- return ngx.say(json.encode(service))
    return service
end
local function fetchinstance_backendip_cache(instancename) 
    local ipadd  = "127.0.0.1";
    local serevicelists = json.decode(ngx.shared.servicelist:get("servicelist"))
    for _, v in pairs(serevicelists) do
       for _, instance in pairs(v) do
         -- if instance.
          if instance.instancename ==  instancename then
             ipadd = instance.instance
             break;
          end
       end
    end
    return ipadd
 end
local function fetchinstance_backendip(instancename) 
   local ipadd  = "127.0.0.1";
   local serevicelists = get_service_lists();
   for _, v in pairs(serevicelists) do
      for _, instance in pairs(v) do
         if instance.instancename ==  instancename then
            ipadd = instance.instance
            break;
         end
      end
   end
   return ipadd
end
return {
    servicelist = get_service_lists,
    serviceips = get_service_ips,
    fetchinstance_backendip = fetchinstance_backendip,
    fetchinstance_backendip_cache =fetchinstance_backendip_cache,
}
local json = require("cjson.safe")
local registryuri = os.getenv("REGISTRY_HOST")
local registryport = os.getenv("REGISTRY_PORT")
local xipdomainname = os.getenv("XIP_DOMAIN")
local xipip = os.getenv("XIP_IP")
local webdomain = os.getenv("WEB_DOMAIN")
-- build registry apps paramas
local function build_eureka_apps()
    local uri = "/eureka/apps/"
    local headers = {["Accept"]="application/json"}
    local params = {path=uri, method="GET", headers=headers}
    return params
end
-- build registry app available ips
local function build_app_upips(service_name)
    local uri = "/eureka/apps/" .. service_name
    local headers = {["Accept"]="application/json"}
    local params = {path=uri, method="GET", headers=headers}
    return params
end
-- parse service list
local function parse_servicelist(body)
    local ok, res_json = pcall(function()
        return json.decode(body)
    end)
    if not ok then
        return nil, "JSON decode error"
    end
    local service = {}
    -- service.upstreams = {}
    for k, v in pairs(res_json.applications.application) do
        local servicename = v["name"]
        local serviceinstances =v["instance"]
        service[servicename] = {}
        for i, instance in pairs(serviceinstances) do
            local status = instance["status"]
            if status == "UP" then
                local ipAddr = instance["ipAddr"]
                local port = instance["port"]["$"]
                local instancename = instance["instanceId"]
                instancename = ngx.re.gsub(instancename,":","_")
                instancename = ngx.re.gsub(instancename,"-","_")
                local instance = ipAddr..":"..port
                local xip_name = instancename
                table.insert(service[servicename], {ip=ipAddr,instance=instance, port=port,instancename= instancename,servicename=servicename,xip = xip_name.."."..xipdomainname})
            end
        end
    end
    return service
end
-- parse service ips
local function parse_service(body)
    local ok, res_json = pcall(function()
        return json.decode(body)
    end)
    if not ok then
        return nil, "JSON decode error"
    end
    local service = {}
    local instances = res_json.application
    local servicename = instances["name"]
    service[servicename] ={};
    local serviceinstances =instances["instance"]
    for i, instance in pairs(serviceinstances) do
            local status = instance["status"]
            if status == "UP" then
                local ipAddr = instance["ipAddr"]
                local port = instance["port"]["$"]
                local instancename = instance["instanceId"]
                instancename = ngx.re.gsub(instancename,":","_")
                instancename = ngx.re.gsub(instancename,"-","_")
                local instance = ipAddr..":"..port
                local xip_name = instancename
                table.insert(service[servicename], {ip=ipAddr,instance=instance, port=port,instancename= instancename,servicename=servicename,xip = xip_name.."."..xipdomainname})
            end
    end
    return service
end
-- fetch env config 
local function envutils()
    local registryuri = os.getenv("REGISTRY_HOST")
    local registryport = os.getenv("REGISTRY_PORT")
    local xipdomainname = os.getenv("XIP_DOMAIN")
    local xipip = os.getenv("XIP_IP")
    local webdomain = os.getenv("WEB_DOMAIN")
    return {
        registryuri = registryuri,
        registryport = registryport,
        xipip = xipip,
        webdomain= webdomain,
    }
end
-- return object for better request
return  {
    build_eureka_apps = build_eureka_apps,
    build_app_upips = build_app_upips,
    parse_servicelist = parse_servicelist,
    parse_service = parse_service,
    envutils = envutils,
}
 

首页模版
index.html

 
<h1>servielist</h1>
<ul>
{% for name, v in pairs(context) do %}
    <li>{{name}}</li>
    {%  for _, v2 in pairs(v) do %} 
    {{v2.instance}} <a href="http://{{v2.xip}}" target="_blank">{{v2.instancename}}</a>
     <br>
    {% end %}
{% end %}
</ul>

一些说明

对于数据或者为了规避cosocket 的问题,使用了timer定时器执行任务,同时数据通过shared cache 共享,部分参数通过env 处理,完整代码没有完全
贴上,只是说明了基本的处理流程,对于xip 的使用可以参考相关资料,实际上我们也可以在access 阶段处理请求,这样可以减少解决set 阶段的cosocket 

参考资料

https://github.com/peterhellberg/xip.name
https://github.com/rongfengliang/xip-rpm
https://github.com/Netflix/eureka/wiki/Eureka-REST-operations