In the development of daily projects, we often use configuration files to save the basic metadata of projects. There are many types of configuration files, such as:JSON
、xml
、yaml
Or even a file in plain text format. No matter what type of configuration data it is, in some scenarios, we may have the need to update the contents of the current configuration file hot, such as: a resident process running with Go, running aWeb Server
Service process.
At this time, if the configuration file changes, how can we let the current program re-read the contents of the new configuration file? Next, we will update the configuration file in the following two ways:
- Use system signals (manual).
- Use
inotify
, listening for file modification events.
Either way, the Go language will be used.goroutine
I intend to use the concept ofgoroutine
Start a new coordination process. The purpose of the new coordination process is to receive the system signal or monitor the event that the file is modified. If you are rightgoroutine
I am not very familiar with the concept of, then I suggest you consult the relevant information first.
Manual, using system signals.
The reason why I call this method manual (Manual
), because the file update requires us to manually inform the currently dependent running program: “hey, buddy! The configuration file has been updated, you have to read the configuration again! !” , we told the way is to send a system signal to the currently running program, so the program’s general idea is as follows:
- In the Go main process, a new one is started.
goroutine
To receive signals. - The new goroutine listens for signals and then updates the configuration file.
In*nix
According to the regulations in the system,USR1
AndUSR2
All belong to user-defined signals. As forUSR1
AndUSR2
Which is more suitable?Wikipedia) also did not give an authoritative answer, so here I according to the convention commonly known as the rules, intend to useUSR1
:
If you have used a Web Server such as Nginx or Apache, then you are certainly somewhat impressed by the strategy of signaling to update the configuration file.
Monitor signal
InGo
It is necessary to monitor system signals in languagesignal
packageNotify()
Method, the method requires at least two parameters, the first parameter requires a channel of system signal type, and the subsequent parameters are one or more system signals to be monitored:
import "os/signal"
Notify(c chan<- os.Signal, sig ...os.Signal)
Therefore, our code is roughly as follows:
package main
import (
"os"
"os/signal"
"syscall"
)
func main() {
//Declare a signal channel with a capacity of 1
sig := make(chan os.Signal, 1)
//Monitor the signal from SIGUSR1
signal.Notify(sig, syscall.SIGUSR1)
}
Here we created a channel with a signal capacity of 1 (channel
), which means that the channel can accommodate at most one signal unit. If a signal unit already exists in the current channel and another signal is received and needs to be sent to the channel, the program will be blocked when sending the signal until the signal in the channel is processed.
In this way, we can accurately process only one signal at a time, and multiple signals need to be queued, which is exactly what I want.
Signal processing
After the system signal is monitored and stored in the channel (sig
, then we need to process the received signal, here we have a new routine (goroutine), the purpose of using the routine is to hope that subsequent tasks do not block the operation of the main process, inGO
In the language, another coordination process is very convenient, only need to call the keyword:go
Just:
go func(){
//new thread
}()
We hope to obtain the system signals in the channel in the new coordination process without stopping. The code is as follows:
go func() {
for {
select {
case <-sig:
//Get the signal in the channel and process the signal
}
}
}()
GO
In languageselect
Statement, whose structure is somewhat similar to that of other languagesswitch
Statement, but the difference is,select
Can only be used for processinggoroutine
The communication operation, andgoroutine
The communication is based onchannel
To achieve, so to put it bluntly:select
Can only be used to process channels (channel
) operation.
Currentselect
Will remain blocked until one of themcase
This will only be executed if the conditions are met.case
Statement under the condition. And here we usefor
Circular structureselect
Statement is in an infinite loop, ifselect
inferiorcase
After receiving a processed signal, when the processing is finished; Because of the outer layerfor
The function of the loop statement is equivalent to resettingselect
When no new signal is received,select
Will again be blocked waiting, cycling.
If you are rightselect
Statement blocking in question, we might as well consider the following code operation:
for {
select {
case <-sig:
//Get the signal in the channel and process the signal
}
fmt.Println("select block test!" )
}
In the aboveselect
After the statement, we try to output a line of string, then ask, “This linefmt.Println()
The function will be displayed in thefor
Run immediately in the loop? “
The answer is yes: no!select
It will block the tone and there will be no output when the program runs untilcase
Match to. it wont hurt you to try it
Hot load configuration
We have already prepared the signal monitoring and the simple work of signal processing. Next we need to refine the code of the signal processing stage and add the logic of loading the configuration file. We will demonstrate loading a simplejson
Configuration file, the path of the file is stored in/tmp/env.json
, the content is relatively simple, only onetest
Fields:
{
"test": "D"
}
At the same time, we need to create and parse thejson
Format matching data structure:
type configBean struct {
Test string
}
We made a statementconfigBean
Structure for andenv.json
Profile fields are mapped one by one, and then simply calljson.Unmarshal()
Function, we can take thisjson
The contents of the document are converted into corresponding onesGo
Language structure content, of course, this is not enough, after parsing we also need to declare a variable to store the structure data, for the program to call elsewhere:
//Global Configuration Variables
var Config config
type config struct {
LastModify time.Time
Data configBean
}
Here, I did not directly putconfigBean
analyticaljson
Data is assigned to a global variable, but is wrapped in another layer and an additional field is declared.LastModify
It is used to store the last modification time of the current file. This has the advantage that each time we receive a signal that we need to update the configuration file, we also need to compare whether the modification of the current file is greater than the last update time. Of course, this is only a small skill for configuration optimization loading.
The following is our code for loading configuration files, and a new one has been added hereloadConfig(path string)
Function to encapsulate all logic for loading configuration files:
//Global Configuration Variables
var Config *config
type configBean struct {
Test string
}
type config struct {
LastModify time.Time
Data configBean // configure content store fields
}
func loadConfig(path string) error {
var locker = new(sync.RWMutex)
//Read the contents of the configuration file
data, err := ioutil.ReadFile(path)
if err ! = nil {
return err
}
//Read File Properties
fileInfo, err := os.Stat(path)
if err ! = nil {
return err
}
//Verify the modification time of the file
if Config ! = nil && fileInfo.ModTime().Before(Config.LastModify) {
return errors.New("no need update")
}
//parse file content
var configBean configBean
err = json.Unmarshal(data, &configBean)
if err ! = nil {
return err
}
config := config{
LastModify: fileInfo.ModTime(),
Data: configBean,
}
//Reassign and update the configuration file
locker.Lock()
Config = config
locker.Unlock()
return nil
}
AboutloadConfig()
Function we need to explain is that although we use locks here, we do not use locks in file reading and writing, only in the assignment phase, because there is no more than one in this scenariogoroutine
Need to operate the same file at the same time, if there are multiple scenes in your scenegoroutine
Concurrent write operations, then to be on the safe side, it is recommended that you add a lock mechanism to the reading and writing of files.
At this point, we have basically completed all the logic for updating the configuration file with the monitoring system signal. Next, we will demonstrate the final result. Before the demonstration, we need tomain
Function to add a little extra code, simulate the main process to become a resident process, here still use channel, the final code is roughly as follows:
func main() {
configPath := "/tmp/env.json"
done := make(chan bool, 1)
//Define Signal Channels
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1)
go func(path string) {
for {
select {
case <-sig:
//Receive the signal and load the configuration file.
_ := loadConfig(path)
}
}
}(configPath)
//Suspend the process until a signal is obtained
<-done
}
Finally, we use a gif picture to demonstrate the final effect:
The final full version code is available here:Github code addressAnd it should be noted that,demo
There are still some minor details in the code, such as: error handling, signal channel closing, etc., please handle it yourself.
Warning: considering the length of the article, we have only implemented the first file update method in this article. In the next article, we will use the second method: usinginotify
Monitor the changes of configuration files to realize automatic update of configuration files. I look forward to your attention.
(360 technical original content, reprint please be sure to keep the two-dimensional code at the end of the article, thank you ~)
About 360 Technology
360 technology is a technology sharing public number created by 360 technology team, pushing technology dry goods every day.
For more technical information, please pay attention to “360 technology” WeChat public number