ceryx+redis实现nginx动态路由功能、动态管理upstream

本教程基于docker-compose的环境下实现的。

一、目的

 

新增一个tomcat节点,或者需要配置nginx的负载均衡,则需要重新配置nginx的config文件中的upstream,然后再重启。

而在生产环境业务高并发的情况下,短暂的重启都是不允许的。因此,需要借助第三方缓存来完成nginx的动态路由功能,支持节点的动态新增、删除、修改等操作。

 

二、适用范围

 

centos7环境,docker-engine 17,docker-compose

 

三、概念

 

Ceryx是一个动态nginx,它是基于openresty的进化版nginx,借助redis完成nginx的动态配置,其中包含了lua、python等多种语言脚本。

Ceryx提供了ceryx-api和ceryx两个容器,其中ceryx又包含了openresty(nginx)。

Ceryx的github项目:https://github.com/sourcelair/ceryx

 

Ceryx源码结构

ceryx+redis实现nginx动态路由功能、动态管理upstream_第1张图片

 

四、ceryx安装

1、下载镜像

 

[root@localhost docker-compose]# docker search ceryx
[root@localhost docker-compose]# docker pull sourcelair/ceryx
[root@localhost docker-compose]# docker pull sourcelair/ceryx-api

2、配置docker-compose.yml

version: '2'
services:
  ceryx:
    image: sourcelair/ceryx:latest
    container_name: ceryx
    ports:
      - "80:80"
      - "443:443"
    external_links:
      - redis
    volumes:
        - /docker-mnt/ceryx/config/nginx-ceryx.conf:/usr/local/openresty/nginx/conf/nginx.conf
        - /docker-logs-mnt/ceryx:/usr/local/openresty/nginx/logs
        - /docker-mnt/ceryx/config/router.lua:/usr/local/openresty/nginx/lualib/router.lua
        - /docker-mnt/ceryx/config/ceryx.conf:/usr/local/openresty/nginx/conf/ceryx.conf
    environment:
        CERYX_DISABLE_LETS_ENCRYPT: "false"
        CERYX_DOCKERIZE_EXTRA_ARGS: -no-overwrite
        CERYX_REDIS_HOST: 192.168.3.48
        CERYX_REDIS_PORT: 6379
        CERYX_REDIS_PASSWORD: "123456"
    command:
      - usr/local/openresty/bin/openresty
      - -g
      - daemon off;      
  ceryx-api:
    image: sourcelair/ceryx-api:latest
    container_name: ceryx-api
    ports:
        - "5555:5555"
    links:
        - ceryx
    external_links:
        - redis
    volumes:
        - /docker-mnt/ceryx/config/db.py:/opt/ceryx/ceryx/db.py
        - /docker-mnt/ceryx/config/app.py:/opt/ceryx/app.py
    environment:
      CERYX_API_HOST: 0.0.0.0
      CERYX_API_HOSTNAME: localhost
      CERYX_API_PORT: 5555
      CERYX_DISABLE_LETS_ENCRYPT: "false"
      CERYX_REDIS_HOST: 192.168.0.1
      CERYX_REDIS_PORT: 6379
      CERYX_REDIS_PASSWORD: "123456"

 

3、配置说明

 

(1)ceryx

ports

作为nginx,则需要监听80端口。如果nginx需要https,则需要443端口。

external_links

Ceryx借助redis,因此需要设置为外部依赖redis

volumes

其中,nginx.conf和logs属于常规挂载。

Nginx.conf是ceryx中的/ceryx/nginx/conf/nginx.conf.tmpl文件的拷贝份,并做一定调整。

route.lua是ceryx的路由规则,我们做了规则的调整,因此需要挂载。

Ceryx.conf是nginx的server配置部分。

Environment

CERYX_REDIS_HOST:redis的ip地址

CERYX_REDIS_PORT:redis的端口

CERYX_REDIS_PASSWORD:redis的密码(如果没有密码,不用配置)

(2)ceryx-api

Ports

开放5555作为api的端口

Links

依赖于ceryx

external_links

外部依赖于redis

Volumes

因为我们对路由规则做了调整,因此api的新增规则也跟着调整,所以挂载db.py和app.py文件。

Environment

CERYX_API_HOST:api可访问host,设置为0.0.0.0即所有ip均可访问。

4、启动ceryx容器

启动前,可以先处理第五部分的“ceryx调整”

[root@localhost docker-compose]# docker-compose -f /docker-compose/docker-compose-ceryx.yml up -d

五、ceryx调整

1、ceryx-api:db.py

db.py可以理解为数据访问工具类。

在原有的insert的基础上,新增insert_for_loadBalance方法,意为负载均衡的规则新增

即同一个upstream内有两个tomcat映射。如:

upstream web{

    server 192.168.0.1:80 weight=5;

    server 192.168.0.2:80 weight=5;

}

 

可见其中重要的两个属性:ip:port映射,weight权重。

 

def insert_for_loadBalance(self, source, target, settings) :
    	"""
    	support for load balancing settings
    	"""
        //1、首先查找是否已经存在对应的source
    	resource = {
            'source': source,
            'target': self.lookup(source, silent=True),
            'settings': self.lookup_settings(source),
        }
    	
    	targetList=[]
        //2、如果已存在,则做target数组的元素新增。否则创建一个新的数组。
    	if resource is not None :
    		resourceTarget=resource.get('target')
    		resourceTarget=resourceTarget.replace("","");
    		if resourceTarget is not None and resourceTarget!="" :
	    		if isinstance(resourceTarget,str) :
	    			targetList=self.jsonStringToList(resourceTarget)
	    		elif isinstance(resourceTarget,list) :
	    			targetList=resourceTarget
 			
    	targetList.append(target)
    	self.insert(source, targetList, settings)

    //特制的jsonString转list
    def jsonStringToList(self,jsonString) :
    	"""
    	function to translate json string to list
    	"""
    	string=jsonString.strip()
    	if string is None or string=='' :
    		return []
    	list=string.split(",")
    	for item in list :
    		index=list.index(item)
    		item=item.replace("[","").replace("]","").replaece("'","").strip()
    		list[index]=item
    	return list

2、ceryx-api:app.py

首先从代码中,可以找到/api/routes对应的method和handler,找到新增规则的handler是create_route.

routes = [
    Route('/api/routes', method='GET', handler=list_routes),
    Route('/api/routes', method='POST', handler=create_route),
    Route('/api/routes/{source}', method='GET', handler=get_route),
    Route('/api/routes/{source}', method='PUT', handler=update_route),
    Route('/api/routes/{source}', method='DELETE', handler=delete_route),

    # Allow trailing slashes as well (GitHub style)
    Route('/api/routes/', method='GET', handler=list_routes, name='list_routes_trailing_slash'),
    Route('/api/routes/', method='POST', handler=create_route, name='create_route_trailing_slash'),
    Route('/api/routes/{source}/', method='GET', handler=get_route, name='get_route_trailing_slash'),
    Route('/api/routes/{source}/', method='PUT', handler=update_route, name='update_route_trailing_slash'),
    Route('/api/routes/{source}/', method='DELETE', handler=delete_route, name='delete_route_trailing_slash'),
]

 

然后在app.py中找到create_route的handler具体实现:

 

def create_route(route: types.Route) -> types.Route:
    ROUTER.insert(**route)
    return http.JSONResponse(route, status_code=201)

 

修改为:

 

def create_route(route: types.Route) -> types.Route:
    ROUTER.insert_for_loadBalance(**route)
    return http.JSONResponse(route, status_code=201)

 

这样,新增route规则的方法,就变成可以给同一个应用的upstream配置多个server映射了。

 

3、ceryx-api使用

调整完以上两个文件后,做好挂载,启动ceryx-api容器就可以访问了。

新增route规则的使用范例:(ipp-route为示例tomcat应用,此命令在linux的可视化工具如ssh上执行)

curl -H "Content-Type: application/json" \
     -X POST \
     -d '{"source":"'192.168.0.1/ipp-route'","target":"ipp-route:8080:w=5","settings":{"enforce_https":false}}' \
     http://192.168.0.1:5555/api/routes

 

范例说明:

 

http://192.168.0.1:5555/api/routes:api的路径,请求时有json参数则新增,否则查询。

Source:route规则的key。我们使用http://192.168.0.1/ipp-route/index.shtml的一部分作为规则的key。

Target:route规则的value,其中ipp-route:8080是containerName:port映射,w=5是权重。

Settings:route规则的配置,其中enforce_https默认为false,即可以不配置settings。

4、redis中存储的route规则

在linux上执行这个脚本成功后,即可以在redis内看到新增的route规则了:

ceryx+redis实现nginx动态路由功能、动态管理upstream_第2张图片

 

5、ceryx:router.lua

 

api的route规则调整为动态负载均衡的机制。那么ceryx则需要调整route规则的解析方法。

以下均为router.lua的部分代码、或需要调整的代码,请仔细比对。

(1)redis访问


local host = ngx.var.host
local scheme=ngx.var.scheme
local requestUri = ngx.var.request_uri
local is_not_https = (scheme ~= "https")
local cache = ngx.shared.ceryx

local prefix = os.getenv("CERYX_REDIS_PREFIX")
if not prefix then prefix = "ceryx" end

-- Prepare the Redis client
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(100) -- 100 ms
local redis_host = os.getenv("CERYX_REDIS_HOST")
if not redis_host then redis_host = "127.0.0.1" end
local redis_port = os.getenv("CERYX_REDIS_PORT")
if not redis_port then redis_port = 6379 end
local redis_password = os.getenv("CERYX_REDIS_PASSWORD")
if not redis_password then redis_password = nil end
local res, err = red:connect(redis_host, redis_port)

-- Return if could not connect to Redis
if not res then
    return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end

if redis_password then
    local res, err = red:auth(redis_password)
    if not res then
        ngx.ERR("Failed to authenticate Redis: ", err)
        return
    end
end

 

由此可知,ceryx支持redis密码访问。

 

(2)根据请求的uri,编辑访问redis的key

-- 字符串分割
function split( str,reps )
    local resultStrList = {}
    string.gsub(str,'[^'..reps..']+',function ( w )
        table.insert(resultStrList,w)
    end)
    return resultStrList
end

-- 获取请求路径中的contextPath
function getContextPath(requestUri)
	local uriArr=split(requestUri,"/")	
	local contextPath ="/"..uriArr[1]
	return contextPath
end

 

-- Construct Redis key
local contextPath=getContextPath(requestUri)
local key = prefix .. ":routes:" .. host..contextPath

其中prefix是前缀,可通过docker-compose配置,默认是ceryx。

 

因此我们的key,可能是ceryx:routes:192.168.0.1/ipp-route

 

(3)缓存机制

拿到了redis中的目标的key,首先第一步,检测缓存中是否存在对应的key的值。

-- Check if key exists in local cache
res, flags = cache:get(key)
if res then
    ngx.var.container_url = res
    return
end

 

(4)从redis中获取的route的规则解析

 

-- Try to get target for host
res, err = red:get(key)
if not res or res == ngx.null then
    -- Construct Redis key for $wildcard
    key = prefix .. ":routes:$wildcard"
    res, err = red:get(key)
	
    if not res or res == ngx.null then
        return ngx.exit(ngx.HTTP_BAD_GATEWAY)
    end
	
    ngx.var.container_url = res
    return
end

-- upstream集合
local upstreamTable = upstreamStringToTable(res)

-- 根据配置的规则(权重),概率选择其中的containerUrl(概率算法)
local targetIndex=getRandomIndex(upstreamTable)

local container=upstreamTable[targetIndex]
local containerUrl=container["containerUrl"]

-- 有些平台只能观察到err级别的日志,因此设置为ngx.ERR
ngx.log(ngx.ERR,"========containerUrl=="..containerUrl.."========")
-- 字符串分割
function lua_string_split(str, split_char)      
	local sub_str_tab = {};  
   
	while (true) do          
		local pos = string.find(str, split_char);    
		if (not pos) then              
			local size_t = table.getn(sub_str_tab)  
			table.insert(sub_str_tab,size_t+1,str);  
		break;    
		end  
   
		local sub_str = string.sub(str, 1, pos - 1);
		local size_t = table.getn(sub_str_tab)  
		table.insert(sub_str_tab,size_t+1,sub_str);  
		local t = string.len(str);  
		str = string.sub(str, pos + 1, t);     
	end      
	return sub_str_tab;  
end

-- 将redis中获取到的upstream字符串转换成table
function upstreamStringToTable(upstreamString)
	local splitList = lua_string_split(upstreamString,",")
	local upstreamTable={}

	for i = 1, #splitList do  
		local item =splitList[i]
		
		item=string.gsub(item,"%[","")	
		item=string.gsub(item,"%]","")
		item=string.gsub(item,"'","")
		
		local itemSplitList=lua_string_split(item,":w=")		
		local containerUrl=itemSplitList[1]		
		local weight=5;
		if itemSplitList[2] and itemSplitList[2] ~= null and itemSplitList[2] ~= ngx.null then
			weight=itemSplitList[2]
		end
		
		local newItem={["containerUrl"]=containerUrl,["weight"]=weight}
		table.insert(upstreamTable,i,newItem)
	end
	return upstreamTable
end

-- 根据权重随机获取container的index
function getRandomIndex(table)

	-- 读取table中的权重的总和
	local maxRandomNum=0
	for index = 1, #table do
		maxRandomNum=maxRandomNum+table[index].weight
	end
	
	-- 将权重总和作为上限,生成随机数
	local randomNum=math.random(maxRandomNum)
	local count=0
	local resultIndex=0;
	
	-- 循环比较权重与随机数的大小
	-- 当index=1时,如果[index=1的权重]<=[随机数],则进入index=2
	-- 当index=2时,如果[index=1的权重]+[index=2的权重]<=[随机数],则进入index=3
	-- 当index=3时,如果([index=1的权重]+[index=2的权重]+[index=3的权重])>[随机数],结束循环
	-- 该index即为我们要找的随机匹配的index
	for index=1,#table do
		count=count+table[index].weight
		if randomNum

 

这样就得到了containerUrl了。

 

(5)nginx跳转路径赋值

最终,将获取到的containerUrl赋值给nginx的变量,nginx内部机制会好好利用变量的。

-- Save found key to local cache for 5 seconds
cache:set(host, containerUrl, 5)
ngx.var.container_url = containerUrl

 

其余几个文件,ceryx.conf、nginx.conf可以挂载,也可以不挂载。ceryx内部机制已经将这两个文件做了挂载,且可用。

 

如果需要调整一些ceryx和nginx的规则,则可以对这两个文件挂载,然后调整后重启即可。

所以,对于ceryx来说,最重要的就是router.lua文件,调整完这个文件即可。

 

六、测试步骤

根据以上调整完毕后,重启ceryx、ceryx-api容器

1、使用ceryx-api插入route规则,或者使用redis可视化直接插入规则,其中key=ceryx:routes:[ip]/[应用名],value=[容器名]:[端口],例如:

key=ceryx:routes:192.168.0.1/ipp-route

value=ipp-route:8080(8080位ipp-route容器内端口),或者192.168.0.1:8001(8001位ipp-route对外发布的端口)

2、查看redis中是否接收到对应的key-value

3、访问应用ipp-route。

 

七、遇到的问题

1、https问题

Ceryx的nginx,默认是https的。其中的ceryx.conf中监听了80和443。

!!莫名其妙解决办法!!

(1)删除ceryx.conf中的ssl初始化

    找到并删除以下内容

    ssl_certificate_by_lua_block {

        auto_ssl:ssl_certificate()

    }

(2)配置tomcatserver.xml

    第一个connector不增加443的redirect   

 

    Engine中新增一行value

 

 

具体如下图所示

 

ceryx+redis实现nginx动态路由功能、动态管理upstream_第3张图片

 

 

修改了tomcat的server.xml,使用当前tomcat创建新的docker镜像,并使用新的镜像创建应用。

 

 

 

 

你可能感兴趣的:(系统运维)