20 | Beyond the Web Server: Privileged Processes and Scheduled Tasks
Hello, I am Wen Ming.
Earlier we introduced OpenResty API, shared dictionary cache and cosocket. The functions they implement are all within the scope of Nginx and Web servers, which can be regarded as providing an implementation with lower development costs and easier maintenance, and provides a programmable Web server.
However, OpenResty doesn't stop there. Let's pick a few today and introduce the functions beyond the Web server in OpenResty. They are scheduled tasks, privileged processes and non-blocking ngx.pipe respectively.
timed task
In OpenResty, we sometimes need to periodically perform certain tasks in the background, such as synchronizing data, cleaning logs, and so on. If you were asked to design, what would you do? The easiest way to think of is to provide an API interface to the outside world, and complete these tasks in the interface; then use the system's crontab to call curl regularly to access this interface, and then realize this requirement in a curve.
However, in this way, not only will there be a sense of fragmentation, but it will also bring higher complexity to operation and maintenance. Therefore, OpenResty provides ngx.timer
to solve this kind of needs. You can ngx.timer
think of it as a client request simulated by OpenResty to trigger the corresponding callback function.
In fact, OpenResty's scheduled tasks can be divided into the following two types:
ngx.timer.at
, used to execute a one-time timing task;ngx.time.every
, used to execute fixed-period timing tasks.
Remember the thinking questions I left at the end of the last class? The question is how to break through the limitation that cosocket cannot be used init_worker_by_lua
in , and this answer is actually ngx.timer
.
The following code starts a scheduled task with a delay of 0. It starts the callback function handler
, and in this function, uses cosocket to visit a website:
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“www.baidu.com", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
This way, we bypass the limitation that cosockets cannot be used at this stage.
Going back to the user needs we mentioned at the beginning of this part, ngx.timer.at
it does not solve the need to run periodically. In the above code example, it is a one-time task.
So, how to achieve periodic operation? On the surface, based on ngx.timer.at
this API, you have two options:
- You can use a while true infinite loop in the callback function, sleep for a while after executing the task, and implement the periodic task by yourself;
- You can also create another new timer at the end of the callback function.
However, before making a choice, we need to clarify one thing: the essence of a timer is a request, although this request is not initiated by the terminal; as for the request, it will exit after completing its own task, and it cannot always Permanent, otherwise it is easy to cause leakage of various resources.
Therefore, the first scheme of using while true to implement periodic tasks by itself is not reliable. Although the second solution is feasible, recursively creating timers is not easy for people to understand.
So, is there a better solution? In fact, the newly added ngx.time.every
API behind OpenResty is designed to solve this problem, and it is a solution closer to crontab.
But the fly in the ointment is that after starting a timer, you have no chance to cancel the scheduled task, after all, ngx.timer.cancel
it is still a todo function.
At this time, you will face a problem: scheduled tasks run in the background and cannot be canceled; if there are a large number of scheduled tasks, it is easy to exhaust system resources.
Therefore, OpenResty provides lua_max_pending_timers
and lua_max_running_timers
these two instructions to limit it. The former represents the maximum value of scheduled tasks waiting to be executed, and the latter represents the maximum value of currently running scheduled tasks.
You can also use the Lua API to get the value of the scheduled task currently waiting to be executed and being executed. The following are two examples:
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
This code will print 1, indicating that there is 1 scheduled task waiting to be executed.
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
This code will print 1, indicating that there is 1 scheduled task running.
privileged process
Then look at the privileged process. We all know that Nginx is mainly divided into master process and worker process, among which, the worker process actually handles user requests. We lua-resty-core
can process.type
get the type of process through the API provided in . For example, you can resty
run the following function with :
$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'
You'll see that it doesn't return a result worker
, but instead single
. This means that resty
the started Nginx has only worker processes and no master process. In fact, the same is true. In the implementation resty
of , you can see that the following line of configuration shuts down the master process:
master_process off;
OpenResty has expanded on the basis of Nginx and added a privileged process: privileged agent. Privileged processes are special:
- It does not listen to any port, which means it will not provide any external services;
- It has the same permissions as the master process, generally speaking, it is
root
the user 's permissions, which allows it to do many tasks that are impossible for the worker process; - Privileged processes can only be started in
init_by_lua
the context ; - In addition, the privileged process only makes sense if it runs in
init_worker_by_lua
the context , because there is no request to trigger, and it will not go to the context ofcontent
,access
etc.
Next, let's look at an example of starting a privileged process:
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent()
if not ok then
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
end
}
After starting the privileged process through this code, and then start the OpenResty service, we can see that there are more privileged processes in the Nginx process:
nginx: master process
nginx: worker process
nginx: privileged agent process
However, if the privilege is only run once in init_worker_by_lua
the stage , which is obviously not a good idea, how should we trigger the privileged process?
That's right, the answer lies in the knowledge just mentioned. Since it does not listen to ports, that is, it cannot be triggered by terminal requests, it can only be ngx.timer
triggered periodically by using what we just introduced:
init_worker_by_lua_block {
local process = require "ngx.process"
local function reload(premature)
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
if not f then
return
end
local pid = f:read()
f:close()
os.execute("kill -HUP " .. pid)
end
if process.type() == "privileged agent" then
local ok, err = ngx.timer.every(5, reload)
if not ok then
ngx.log(ngx.ERR, err)
end
end
}
The above code implements the function of sending the HUP semaphore to the master process every 5 seconds. Naturally, you can also implement more interesting functions on this basis, such as polling the database to see if there are tasks of privileged processes and execute them. Because the privileged process has root authority, this is obviously a bit of a "backdoor" program.
non-blocking ngx.pipe
Finally we look at the non-blocking ngx.pipe. In the code example just mentioned, we use Lua's standard library to execute the external command line and send the signal to the master process:
os.execute("kill -HUP " .. pid)
This operation is naturally blocked. So, in OpenResty, is there a non-blocking way to call external programs? After all, know that if you're using OpenResty as a complete development platform rather than a web server, this is exactly what you need.
To this end, lua-resty-shell
the library came into being, and using it to call the command line is non-blocking:
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout)
This code can be regarded as another way of writing hello world, it calls the system echo
command to complete the output. Similarly, you can use resty.shell
instead of os.execute
calling .
We know that lua-resty-shell
the underlying implementation of , relies lua-resty-core
on the [ ngx.pipe ] API in , so lua-resty-shell
this hello wrold
example of using to print out ngx.pipe
can be written as follows:
$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'
This is actually lua-resty-shell
the underlying implementation code. You can ngx.pipe
check the documentation and test cases for more usage methods, so I won't go into details here.
write at the end
At this point, I have finished the main content of today. From the above functions, we can see that OpenResty is also trying to move closer to the general platform on the premise of making a better Nginx. We hope that developers can try to unify the technology stack and use OpenResty to solve it. development needs. This is very friendly to operation and maintenance, because only one OpenResty is deployed, and the maintenance cost is lower.
Finally, I leave you with a thought question. Since there may be multiple Nginx workers, the timer will run once in each worker, which is unacceptable in most scenarios. How should we ensure that the timer runs only once?
Welcome to leave a message to talk about your solution, and you are welcome to share this article with your colleagues and friends. We can communicate and make progress together.