OpenResty's Killer Feature: Dynamic

API7.ai

January 12, 2023

OpenResty (NGINX + Lua)

So far, We're almost done with OpenResty performance-related content. Mastering and flexibly applying these optimization techniques can greatly improve our code's performance. Today, in the last part of performance optimization, let's learn about a commonly underestimated capability in OpenResty: "dynamic".

Let's start by looking at what dynamic is and how it relates to performance. Dynamic in this context means that programs can modify parameters, configurations, and even their code at runtime, without reloading. Specifically, in NGINX and OpenResty, you can change upstream, SSL certificates, and rate-limit thresholds without restarting the service, achieving dynamic. As for the relationship between dynamic and performance, it is clear that if these types of operations cannot be done dynamically, then frequent reloads of NGINX services will naturally result in performance loss.

However, we know that the open-source version of NGINX does not support dynamic features, so you have to change upstream SSL certificates by modifying the configuration file and restarting the service to make them effective. The NGINX Plus (commercial version of NGINX) provides some dynamic capabilities, and you can use the REST API to update, but this is a less-than-radical improvement at best.

In OpenResty, these shackles do not exist, and dynamic is the killer feature of OpenResty. You may wonder why OpenResty, based on NGINX, can support dynamic. The reason is simple: NGINX logic is done through C modules, while OpenResty is done through Lua, a scripting language. One of the advantages of scripting languages is that they can be changed dynamically at runtime.

Load code dynamically

Here's how to dynamically load Lua code in OpenResty.

resty -e 'local s = [[ngx.say("hello world")]]
local func, err = loadstring(s)
func()'

We can see that in just a few lines of code, we can turn a string into a Lua function and get it running. Let's take a closer look at these lines of code further:

  • First, we declare a string whose content is a piece of Lua code that prints out hello world;
  • Then, using the loadstring function in Lua, turn the string object into the function object func.
  • Finally, add parentheses to the function name to execute the func and print out hello world.

Of course, we can also extend more interesting and practical functions based on this code. Next, I'll take you to have a try.

Function 1: FaaS

First is FaaS (Function-as-a-Service), which has recently been a very popular technology direction. Let's see how to implement it in OpenResty. In the code just mentioned, the string is a Lua code. We can also change it into a Lua function:

local s = [[
 return function()
     ngx.say("hello world")
end
]]

As we said, functions are first-class citizens in Lua, and this code returns an anonymous function. When executing this anonymous function, we use pcall to provide a layer of protection. pcall will run the function in protected mode and catch the exception. If it is normal, it will return true and the execution result. If it fails, it will return false and error information, which is the following code:

local func1, err = loadstring(s)
local ret, func = pcall(func1)

Naturally, if you combine the above two parts, you will get a complete and operable example:

resty -e 'local s = [[
 return function()
    ngx.say("hello world")
end
]]
local  func1 = loadstring(s)
local ret, func = pcall(func1)
func()'

To go a step further, we can change the string s containing functions to a form that users can specify and add the conditions for its execution. This is the prototype of FaaS. Here, I provide a complete implementation. If interested in FaaS and want to continue your research, go through the link to learn more.

Function 2: Edge Computing

OpenResty's dynamic can be used for FaaS, making the dynamicity of the script language refined to the function level, and playing a dynamic role in edge computing.

Because of these advantages, we can extend the tentacles of OpenResty from the fields of API gateway, WAF (Web Application Firewall), web server, and other server ends to the edge nodes closest to users, such as IoT devices, CDN edge nodes, routers and so on.

This is not just a fantasy. OpenResty has been widely used in the above fields. Taking CDN edge nodes as an example, Cloudflare, the largest user of OpenResty, has realized dynamic control of CDN edge nodes with the help of OpenResty's dynamic characteristics for a long time.

Cloudflare's approach is similar to the above principle of dynamically loading code, which can be roughly divided into the following steps:

  • First, obtain the changed code files from the key-value database cluster. The method can be background timer polling or "publish-subscribe" mode to monitor;
  • Then, replace the old file on the local disk with the updated code file and update the cache loaded in memory using the loadstring and pcall methods;

This way, the next client request to be processed will go through the updated code logic. Of course, the practical application should consider more details than the above steps, such as version control and rollback, exception handling, network interruption, edge node restart, etc., but the overall process is unchanged.

If we move Cloudflare's approach from CDN edge nodes to other edge scenarios, we can dynamically assign a lot of computing power to edge node devices. This can not only make full use of the computing power of edge nodes but also enable users to get faster responses to requests because the edge node will process the original data and then summarize it to the remote server, which greatly reduces the amount of data transmission.

However, to do an excellent job in FaaS and edge computing, OpenResty's dynamic is only a good foundation. You also need to consider the improvement of your surrounding ecology and the participation of manufacturers, which is not just a technical category.

Dynamic Upstream

Now, let's pull our thoughts back to OpenResty to see how to achieve dynamic upstream. lua-resty-core provides a library of ngx.barancer to set up the upstream. It needs to be placed in the balancer stage of OpenResty to run:

balancer_by_lua_block {
    local balancer = require "ngx.balancer"
    local host = "127.0.0.2"
    local port = 8080

    local ok, err = balancer.set_current_peer(host, port)
    if not ok then
        ngx.log(ngx.ERR, "failed to set the current peer: ", err)
        return ngx.exit(500)
    end
}

The set_current_peer function sets up the upstream IP address and port. However, we would like to point out that the domain name is not supported here. We need to use the lua-resty-dns library to make a layer of analysis for the domain name and IP.

However, ngx.balancer is relatively low. Although it can be used to set upstream, dynamic upstream realization is far from simple. Therefore, two functions are needed in front of ngx.balancer:

  • First, decide whether the upstream selection algorithm is consistent hash or roundrobin;
  • The second is the upstream health check mechanism, which needs to eliminate unhealthy upstream and re join it when unhealthy upstream becomes healthy.

The library of OpenReasty's official lua-resty-balancer contains two types of algorithms: resty.chash and resty.roundrobin to complete the first function, and it has lua-resty-upstream-healthcheck to try to complete the second function.

However, there are still two problems.

The first point is the lack of complete implementation of the last mile. Turn ngx.balancer, lua-resty-balancer, and lua-resty-upstream-healthcheck to combine the functions of dynamic upstream, but still need some workload, which stops most developers.

Second, the implementation of lua-resty-upstream-healthcheck is not complete. There is only passive health check but no active health check.

The clients' requests trigger the passive health check here and then analyze the upstream return value as a condition to determine whether the health is good. Whether the upstream is healthy is unknown if there is no client request. Active health checks can remedy this defect. It uses ngx.timer to periodically poll the specified upstream interface to detect health status.

Therefore, in actual practice, we usually recommend using the lua-resty-healthcheck to complete upstream health checks. Its advantage is that it includes active and passive health checks, and has been verified in multiple projects with higher reliability.

Furthermore, the emerging microservice API gateway Apache APISIX has made a complete implementation of dynamic upstream based on the lua-resty-upstream-healthcheck. We can refer to its implementation. There are only 400 lines of code in total. You can easily peel it off and put it into your project for use.

Summary

With regard to the dynamic of OpenResty, in what areas and scenarios can you take advantage of it? You can also expand the contents of each part introduced in this chapter for more detailed and in-depth analysis.

You are welcome to share this article and learn and make progress with more people.