做了这3个调整,LeanEngine 可以轻松地撑起5倍流量了

做了这3个调整,LeanEngine 可以轻松地撑起5倍流量了

王滨王滨

0. 背景交代

云引擎(LeanEngine)是 LeanCloud 推出的服务端托管平台。提供了多种运行环境(Node.js, Python, PHP, Java 等)来运行服务端程序。你只需要提供服务端的业务逻辑(网站或云函数等),而服务端的多实例负载均衡,不中断服务的平滑升级等都由云引擎提供支持。

总之你可以在 LeanCloud 上跑自己的代码,处理 HTTP 请求。用最近的 buzzword 说,LeanEngine 是 LeanCloud 提供的一个 CaaS(Container as a Service)平台(笑

用户的容器不可能直接暴露在公网上的,在用户的容器与公网入口之间的东西,就是这篇文章要说明的。



1. 改造之前

可能很多人觉得,这不就是一个 nginx 的事么…… 答对了!

确实在改造前,云引擎的入口就是几台 nginx,然后路由到集群中的 hipache 上,nginx 仅仅负责 SSL 卸载以及域名绑定的工作,剩下的复杂的路由工作就交给了 hipache,由 hipache 来寻找用户的容器到底在哪,以及路由过去。

这个架构非常简单,但是有3点问题:

  1. 入口处的 nginx 不易扩展。我们的用户还是很厉害的,做一下活动,流量一下子变成了平时高峰的 5 倍以上,因此 nginx 的机器配置都是很过量了,平时的负载很低,也不敢减掉,高峰的时候却看着 70% 的 CPU 占用额头上冒汗。
  2. 刚才说过 nginx 这一层要负责 SSL 卸载,所以也必须要负责用户绑定域名的解析(想想为什么?),不能丢给 hipache。这样每一次有用户来绑定域名时,后台会走一个流程(我们用了 rundeck ),从数据库里读取绑定的信息,生成 nginx 配置,然后 ssh 到入口机器上对 nginx 做 reload。每一次域名绑定速度都非常慢,也经常性的需要人工干预。
  3. LeanCloud 内部所有的服务都是跑在 Mesos 集群上的,hipache 也不例外。当 hipache 部署或者扩容缩容的时候,nginx 的 upstream 也要跟着变。现在的实现跟域名绑定一样,有一个脚本监控着 Marathon 上的实例信息,如果变化了,就生成新的 nginx upstream 配置文件,然后 reload nginx。因为 LeanEngine 目前允许 WebSocket 连接,nginx 在 reload 之后老 worker 会残留很长时间,可能在两次部署之后还会残留。老 worker 会挤占内存,直到最后 nginx 无法再 reload,需要人工干预。


2. 目标

可扩展的问题我们已经解决了,我们要做的就是把 nginx 搬到 Mesos 上,随时按照需求增减实例。

而用户的域名绑定不能再走这么复杂的流程了,应该要做到数据库中有记录,nginx 就能正确的服务。


最后一条 reload 的问题,则需要 nginx 可以动态的调整 upstream,不能依靠 reload。



3. 引入 OpenResty

因为 hipache 是 Node.js 项目,代码不太好维护,也很容易出现故障(之前有一次故障,用户请求触发了一个 hipache 的 bug,而且异常没有 catch 住,导致 hipache 实例不停重启,无法服务),因此我们的 95 后 dev @王子亭 用 OpenResty 重写了 hipache,新项目在我们内部叫 hogplum。hogplum 上线后效果很好,效率明显的比魔改 hipache 要高,代码量也减少了非常多(现在只有几百行),可以很容易的掌控。

有了之前的成功经验做背书,我们决定把入口处的 nginx 也改成 OpenResty,将域名绑定的工作以及 upstream 更新做成动态的。


开发的过程并没有什么值得一说的,都是一些常用的逻辑,通过 API 取数据什么的。这里列举一些比较有意思的点:
  1. 动态更新 upstream 是通过 ngx_http_dyups_module 模块实现的
  2. 为了保留来源 IP 的信息,我们用了 proxy 协议,后文会说道
  3. 在实现元数据缓存的时候,发现 lua-nginx-lrucache 不能保存 false 值,提了一个 PR;另外 ngx.shared 的共享存储提供了 TTL,但是会比较激进地剔除掉过期的值(ngx.shared 的 API 提供了获得过期值的方法),这并不是我们想要的,于是用了 flags 存了过期时间自己实现了 TTL。

最终的效果大概是这样:

server {
    include listen.conf;
    include common/ssl-common.conf;
    server_name _;
    ssl_certificate path/to/leanapp.cn.crt;
    ssl_certificate_key path/to/leanapp.cn.key;
    root leanapp.cn/static/;

    # 用来处理自定义域名的 SSL 证书
    ssl_certificate_by_lua_file 'lua/leanapp/ssl.lua';

    location / {
        # 用来解析自定义域名与用户容器之间的关系
        set $domain '';
        rewrite_by_lua_file 'lua/leanapp/domain.lua';
        proxy_set_header X-LC-Domain $domain;

        # 动态代理到 Mesos 集群里的 hogplum 实例上
        dyups_interface;
        set $marathon_app 'marathon:8080#lean-engine/hogplum:8080';
        set $upstream '';
        access_by_lua_file 'lua/marathon-app.lua';
        proxy_pass http://$upstream;
    }
}
-- lua/leanapp/domain.lua
local meta = require 'leanapp.meta'

local host = ngx.var.http_host;
if not host then
    ngx.redirect 'http://leanapp.cn'
    return
end

local domain = meta.get_subdomain(host)
if not domain then
    ngx.redirect 'http://leanapp.cn'
else
    -- ngx.var.logname = host
    ngx.var.logname = domain
    ngx.var.domain = domain
end
-- lua/leanapp/ssl.lua
local meta = require 'leanapp.meta'
local ngx_ssl = require 'ngx.ssl'

local host = ngx_ssl.server_name()
if not host then
    return
end
local cert, key = meta.get_ssl(host)
if not cert or not key then
    return
end

ngx_ssl.set_cert(cert)
ngx_ssl.set_priv_key(key)
-- lua/marathon-app.lua
local dyups = require 'ngx.dyups'

local marathon = require 'marathon'
local die = require('utils').die

local marathon_app = ngx.var.marathon_app;
local upstream = marathon.get_upstream(marathon_app)

if not upstream then
  die('No upstream for ' .. marathon_app)
end

local target = marathon_app:gsub('[/:#]', '-')
dyups.update(target, upstream)
ngx.var.upstream = target 

marathon 模块因为代码太长,以及 leanapp.meta 里面有涉及敏感的信息,就不贴在这里了。

在文章尾部附上了 marathon 路由的地址,感兴趣的可以调研下。



4. 那入口在哪?

折腾了半天,终于把 nginx 放到 Mesos 上了。那么用户怎么访问呢?

还是缺一个固定的入口,只不过改造之后这个入口只需要做转发就够了,需要的资源非常少。

因为需要通过 Proxy 协议保留客户端的 ip 地址信息,我们没有用 LVS 而是用 HAProxy 来做转发,在调优之后(减少 HAProxy 的缓冲区大小,打开 splice),HAProxy 在满载的时候只占用了很少的用户态内存(10M),剩下的都是内核 TCP 占用了。实际上每台机器 1核1G 就可以了,给了 2G 只是为了安全。目前我们的入口有4个,可以通过 `dig leanapp.cn` 看到。

在 nginx 扩容/缩容后,会有一个类似之前的机制对 haproxy 做 reload,可以当作是简易的 marathon-lb



5. 额外的优化空间

改造后的整个入口是这个样子的

可以看到一个请求走了两个 OpenResty 实例(nginx 和 hogplum)。这是因为 nginx 和 hogplum 是分别由运维组和云引擎组负责的,hipache 在改造成 hogplum 的时候并没有重新考虑负责的边界。之后自定义域名的路由(以及 SSL 卸载)会合并到 hogplum 中,就不会再看到 hogplum 之前的 nginx 了。

nginx 这一层也可以去掉云引擎的业务代码,仅做 nginx 应该做的路由工作,代替现在的 API 入口(这也是最开始的目标)。



6. 效果

改造之后,我们又等了一波用户的活动,果然 nginx 开始过载。

因为 LeanCloud 的统计服务有重试机制,暂时的上报失败不会有影响,于是我们迅速的对统计服务进行缩容,将集群中的资源让给 nginx,整个过程只点了几次鼠标。 HAProxy 因为只转发没有业务逻辑,消耗的资源很少,还有很多余量,不是整个系统的瓶颈。



7. 开源

marathon 路由的部分已经开源,可以戳这里: leancloud/resty-marathon-lb

文章被以下专栏收录
4 条评论
推荐阅读