本教程基于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源码结构
[root@localhost docker-compose]# docker search ceryx
[root@localhost docker-compose]# docker pull sourcelair/ceryx
[root@localhost docker-compose]# docker pull sourcelair/ceryx-api
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"
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的密码(如果没有密码,不用配置)
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均可访问。
启动前,可以先处理第五部分的“ceryx调整”
[root@localhost docker-compose]# docker-compose -f /docker-compose/docker-compose-ceryx.yml up -d
五、ceryx调整
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
首先从代码中,可以找到/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映射了。
调整完以上两个文件后,做好挂载,启动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。
在linux上执行这个脚本成功后,即可以在redis内看到新增的route规则了:
api的route规则调整为动态负载均衡的机制。那么ceryx则需要调整route规则的解析方法。
以下均为router.lua的部分代码、或需要调整的代码,请仔细比对。
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密码访问。
-- 字符串分割
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
拿到了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
-- 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了。
最终,将获取到的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。
Ceryx的nginx,默认是https的。其中的ceryx.conf中监听了80和443。
!!莫名其妙解决办法!!
找到并删除以下内容
ssl_certificate_by_lua_block {
auto_ssl:ssl_certificate()
}
(2)配置tomcat的server.xml
第一个connector不增加443的redirect
Engine中新增一行value
具体如下图所示
修改了tomcat的server.xml,使用当前tomcat创建新的docker镜像,并使用新的镜像创建应用。