Xnginx – nginx cluster visual management tool

Before the nginx management of the team, the operation and maintenance students modified the configuration file every time and then restarted it. It was very inconvenient. They always wanted to find a tool that could easily manage the nginx cluster. They searched the web and found nothing available, so they designed and developed one by themselves.

Effect preview

You can manage group nodes and configuration files. After modification, you can restart all nodes with one click, and an error will be prompted when the configuration file is wrong, which will not affect online services.

2. Cluster node management

3 . Viewing cluster node logs

Design ideas

Data structure: an nginxgroup has multiple nginxnodes and shares the same configuration file.

Distributed architecture: manager node + agent node + web management. Each nginx machine deploys an agent. After the agent is started, it automatically registers with the manager. Through the web, you can set the group to which the agent belongs and manage the configuration file of the group.

After the configuration file is changed, the manager generates the configuration file and distributes it to the surviving agent. After checking that it is OK, the manager controls the agent to restart nginx.

Key technical points

Distributed management

Generally, distributed can be realized by means of zookeeper and other registration centers. As a java project, Eureka server can be used:

Manager joins Eureka dependency:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

Then add @ enableeurekaserver in the portal

Add agent registration configuration:

eureka:
	instance:
		prefer-ip-address: true
	client:
		service-url:
			defaultZone: http://admin:admin@ip:3002/eureka/

The manager node obtains the surviving agent, and can obtain the registered agent through eurekaservercontextholder. At the same time, it can automatically discover new nodes through scheduled tasks.

public class NginxNodeDiscover {

    private static final String AGENT_NAME = "XNginxAGENT";

    private PeerAwareInstanceRegistry getRegistry() {
        return getServerContext().getRegistry();
    }

    private EurekaServerContext getServerContext() {
        return EurekaServerContextHolder.getInstance().getServerContext();
    }

    @Autowired
    NginxNodeRepository NginxNodeRepository;

    @Scheduled(fixedRate = 60000)
    public void discoverNginxNode() {
        List<String> nodes = getAliveAgents();
        nodes.stream().forEach(node->{
            if(!NginxNodeRepository.findByAgent(node).isPresent()){
                NginxNode NginxNode = new NginxNode();
                NginxNode.setAgent(node);
                NginxNode.setName(node);
                NginxNodeRepository.save(NginxNode);
            }
        });
    }

    public List<String> getAliveAgents() {
        List<String> instances = new ArrayList<>();
        List<Application> sortedApplications = getRegistry().getSortedApplications();
        Optional<Application> targetApp = sortedApplications.stream().filter(a->a.getName().equals(AGENT_NAME)).findFirst();
        if(targetApp.isPresent()){
            Application app = targetApp.get();
            for (InstanceInfo info : app.getInstances()) {
                instances.add(info.getHomePageUrl());
            }
        }
        return instances;
    }
}

RPC call

The manager needs to control the agent. According to the simplest scheme, the agent provides a rest service, which can be called directly after obtaining the address from Eureka. In addition, feign can be used to facilitate the call.

Define interface:

public interface NginxAgentManager {

    @RequestLine("GET /Nginx/start")
     RuntimeBuilder.RuntimeResult start() ;

    @RequestLine("GET /Nginx/status")
     RuntimeBuilder.RuntimeResult status() ;

    @RequestLine("GET /Nginx/reload")
     RuntimeBuilder.RuntimeResult reload() ;

    @RequestLine("GET /Nginx/stop")
     RuntimeBuilder.RuntimeResult stop();

    @RequestLine("GET /Nginx/testConfiguration")
     RuntimeBuilder.RuntimeResult testConfiguration();

    @RequestLine("GET /Nginx/kill")
     RuntimeBuilder.RuntimeResult kill() ;

    @RequestLine("GET /Nginx/restart")
     RuntimeBuilder.RuntimeResult restart() ;

    @RequestLine("GET /Nginx/info")
     NginxInfo info();

    @RequestLine("GET /Nginx/os")
     OperationalSystemInfo os() ;

    @RequestLine("GET /Nginx/accesslogs/{lines}")
    List<NginxLoggerVM> getAccesslogs(@Param("lines") int lines);

    @RequestLine("GET /Nginx/errorlogs/{lines}")
    List<NginxLoggerVM> getErrorLogs(@Param("lines") int lines);

}

Functions of agent:

@RestController
@RequestMapping("/Nginx")
public class NginxResource {

   ...

    @PostMapping("/update")
    @Timed
    public String  update(@RequestBody NginxConf conf){
        if(conf.getSslDirectives()!=null){
            for(SslDirective sslDirective : conf.getSslDirectives()){
                NginxControl.conf(sslDirective.getCommonName(),sslDirective.getContent());
            }
        }
        return updateConfig(conf.getConf());
    }

    @GetMapping("/accesslogs/{lines}")
    @Timed
    public List<NginxLoggerVM> getAccesslogs(@PathVariable Integer lines) {
        return NginxControl.getAccessLogs(lines);
    }


}

Manager call;

A proxy instance is created, where nodeurl is the URL address of the agent node

    public NginxAgentManager getAgentManager(String nodeUrl){
        return Feign.builder()
            .options(new Request.Options(1000,3500))
            .retryer(new Retryer.Default(5000,5000,3))
            .requestInterceptor(new HeaderRequestInterceptor())
            .encoder(new GsonEncoder())
            .decoder(new GsonDecoder())
            .target(NginxAgentManager.class,nodeUrl);
    }

Then the call is simple, for example, to start group:

public void start(String groupId){
        operateGroup(groupId,((conf,node) -> {
            NginxAgentManager manager = getAgentManager(node.getAgent());

            String result = manager.update(conf);
            if(!result.equals("success")){
                throw new XNginxException("node "+ node.getAgent()+" update config file Failed!");
            }

            RuntimeBuilder.RuntimeResult runtimeResult =   manager.start();
            if(!runtimeResult.isSuccess()){
                throw new XNginxException("node "+ node.getAgent()+" start Failed,"+runtimeResult.getOutput());
            }
        }));
    }

    public void operateGroup(String groupId,BiConsumer<NginxConf,NginxNode> action){

        List<String> alivedNodes = nodeDiscover.getAliveAgents();
        if(alivedNodes.size() == 0){
            throw new XNginxException("no alived agent!");
        }
        List<NginxNode> NginxNodes = nodeRepository.findAllByGroupId(groupId);
        if(NginxNodes.size() ==0){
            throw new XNginxException("the group has no Nginx Nodes!");
        }

        NginxConf conf = NginxConfigService.genConfig(groupId);

        for(NginxNode node : NginxNodes){

            if(!alivedNodes.contains(node.getAgent())){
                continue;
            }

            action.accept(conf,node);
       }
    }

Nginx configuration management

The core of nginx is various directive (instructions), and the core is Vhost and location.

Let's first define Vhost:

public class VirtualHostDirective implements Directive {

	private Integer port = 80;
	private String aliases;
	private boolean enableSSL;
	private SslDirective sslCertificate;
	private SslDirective sslCertificateKey;
	private List<LocationDirective> locations;

	private String root;
	private  String index;
	private String access_log;
}

The design idea of the core locationdirective is that passaddress stores the target address of location, which can be URL or upstream. It can be distinguished by type. At the same time, if there is upstream, it can set the load information through proxy.

public class LocationDirective {

	public static final String PROXY = "PROXY";
	public static final String UWSGI = "UWSGI";
	public static final String FASTCGI = "FASTCGI";
	public static final String COMMON = "STATIC";

	private String path;

	private String type = COMMON;

	private ProxyDirective proxy;

	private List<String> rewrites;

	
	private String advanced;

	private String passAddress;
	
	}
	

Let's look at proxydirective. Balance is used to distinguish between ordinary URL and upstream. If it is upstream, servers are the servers that store the load.

public class ProxyDirective implements Directive {

	public static final String BALANCE_UPSTREAM = "upstream";
	public static final String BALANCE_URL = "url";


	private String name;

	private String strategy;

	/**
	 *  Upstream balance type : upsteam,url
	 */
	private String balance = BALANCE_UPSTREAM;

	private List<UpstreamDirectiveServer> servers;
	
	}

Historical data import

Once the configuration information is available, it can be imported into the system through parsing. Parsing is the conventional text parsing, which will not be repeated here.

The core idea is to divide the configuration file into blocks by matching braces, and then extract information through regularization. For example, the following code splits the server {...}

private List<String> blocks() {
        List<String> blocks = new ArrayList<>();
        List<String> lines = Arrays.asList(fileContent.split("\n"));

        AtomicInteger atomicInteger = new AtomicInteger(0);
        AtomicInteger currentLine = new AtomicInteger(1);
        Integer indexStart = 0;
        Integer serverStartIndex = 0;
        for (String line : lines) {
            if (line.contains("{")) {
                atomicInteger.getAndIncrement();
                if (line.contains("server")) {
                    indexStart = currentLine.get() - 1;
                    serverStartIndex = atomicInteger.get() - 1;
                }
            } else if (line.contains("}")) {
                atomicInteger.getAndDecrement();
                if (atomicInteger.get() == serverStartIndex) {
                    if (lines.get(indexStart).trim().startsWith("server")) {
                        blocks.add(StringUtils.join(lines.subList(indexStart,currentLine.get()),"\n"));
                    }
                }
            }
            currentLine.getAndIncrement();
        }
        return blocks;
    }

Profile generation

Configuration files are generally generated through the template engine, and here is no exception. The velocity library is used.

    public static StringWriter mergeFileTemplate(String pTemplatePath,Map<String,Object> pDto) {
        if (StringUtils.isEmpty(pTemplatePath)) {
            throw new NullPointerException("????????????");
        }
        StringWriter writer = new StringWriter();
        Template template;
        try {
            template = ve.getTemplate(pTemplatePath);
        } catch (Exception e) {
            throw new RuntimeException("????????",e);
        }
        VeLocityContext context = VeLocityHelper.convertDto2VeLocityContext(pDto);
        try {
            template.merge(context,writer);
        } catch (Exception e) {
            throw new RuntimeException("????????",e);
        }
        return writer;
    }

Define template:

#if(${config.user})user ${config.user};#end
#if(${config.workerProcesses}== 0 )
worker_processes auto;
#else
worker_processes  ${config.workerProcesses};
#end
pid        /opt/xNginx/settings/Nginx.pid;

events {
    multi_accept off;
    worker_connections ${config.workerConnections};
}

...

Generate configuration file;

    public static StringWriter buildNginxConfString(ServerConfig serverConfig,List<VirtualHostDirective> hostDirectiveList,List<ProxyDirective> proxyDirectiveList) {
        Map<String,Object> map = new HashMap<>();
        map.put("config",serverConfig);
        map.put("upstreams",proxyDirectiveList);
        map.put("hosts",hostDirectiveList);
        return VeLocityHelper.mergeFileTemplate(Nginx_CONF_VM,map);
    }

Manage web

The management web is based on ng Alain framework and developed with typescript + angular MVVM, which is no essential difference from the back end

The development is relatively simple and will not be repeated here.

Summary

At present, only basic management functions have been realized, which can be supplemented and improved as needed, such as supporting business, person in charge and other information management and maintenance.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>