{"id":19155936,"url":"https://github.com/lyokha/nginx-combined-upstreams-module","last_synced_at":"2025-05-07T07:33:30.896Z","repository":{"id":15556575,"uuid":"18291698","full_name":"lyokha/nginx-combined-upstreams-module","owner":"lyokha","description":"nginx module for building combined upstreams","archived":false,"fork":false,"pushed_at":"2025-04-15T16:30:00.000Z","size":327,"stargazers_count":9,"open_issues_count":1,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-15T16:52:33.051Z","etag":null,"topics":["nginx","upstrand","upstream"],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lyokha.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-03-31T12:06:04.000Z","updated_at":"2025-04-15T16:05:28.000Z","dependencies_parsed_at":"2024-08-18T09:47:59.419Z","dependency_job_id":"67193aa3-3459-450a-b5f6-404a8c499a17","html_url":"https://github.com/lyokha/nginx-combined-upstreams-module","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyokha%2Fnginx-combined-upstreams-module","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyokha%2Fnginx-combined-upstreams-module/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyokha%2Fnginx-combined-upstreams-module/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyokha%2Fnginx-combined-upstreams-module/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lyokha","download_url":"https://codeload.github.com/lyokha/nginx-combined-upstreams-module/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252833918,"owners_count":21811276,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nginx","upstrand","upstream"],"created_at":"2024-11-09T08:32:43.949Z","updated_at":"2025-05-07T07:33:30.859Z","avatar_url":"https://github.com/lyokha.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"Nginx Combined Upstreams module\n===============================\n\n\u003c!--[![Build Status](https://travis-ci.com/lyokha/nginx-combined-upstreams-module.svg?branch=master)](https://travis-ci.com/lyokha/nginx-combined-upstreams-module)--\u003e\n[![Build Status](https://github.com/lyokha/nginx-combined-upstreams-module/workflows/CI/badge.svg)](https://github.com/lyokha/nginx-combined-upstreams-module/actions?query=workflow%3ACI)\n\nThe module introduces three directives *add_upstream*,\n*combine_server_singlets*, and *extend_single_peers* available inside upstream\nconfiguration blocks, and a new configuration block *upstrand* for building\nsuper-layers of upstreams. Additionally, directive *dynamic_upstrand* is\nintroduced for choosing upstrands in run-time.\n\nTable of contents\n-----------------\n\n- [Directive add_upstream](#directive-add_upstream)\n- [Directive combine_server_singlets](#directive-combine_server_singlets)\n- [Directive extend_single_peers](#directive-extend_single_peers)\n- [Block upstrand](#block-upstrand)\n- [Directive dynamic_upstrand](#directive-dynamic_upstrand)\n- [Build and test](#build-and-test)\n- [See also](#see-also)\n\nDirective add_upstream\n----------------------\n\nPopulates the host upstream with servers listed in an already defined upstream\nspecified by the mandatory 1st parameter of the directive. The server attributes\nsuch as weights, max_fails and others are kept in the host upstream. Optional\nparameters may include values *backup* to mark all servers of the sourced\nupstream as backup servers and *weight=N* to calibrate weights of servers of the\nsourced upstream by multiplying them by factor *N*.\n\n### An example\n\n```nginx\nupstream  combined {\n    add_upstream    upstream1;            # src upstream 1\n    add_upstream    upstream2 weight=2;   # src upstream 2\n    server          some_another_server;  # if needed\n    add_upstream    upstream3 backup;     # src upstream 3\n}\n```\n\nDirective combine_server_singlets\n---------------------------------\n\nProduces multiple *singlet upstreams* from servers so far defined in the host\nupstream. A singlet upstream contains only one active server whereas other\nservers are marked as backup or down. If no parameters were passed then the\nsinglet upstreams will have names of the host upstream appended by the ordering\nnumber of the active server in the host upstream. Optional 2 parameters can be\nused to adjust their names. The 1st parameter is a suffix added after the name\nof the host upstream and before the ordering number. The 2nd parameter must be\nan integer value which defines *zero-alignment* of the ordering number. For\nexample, if it has value 2 then the ordering numbers could be\n``'01', '02', ..., '10', ... '100' ...``.\n\nTo mark secondary servers as down rather than backup, use another optional\nparameter *nobackup*. This parameter must be put in the end, after all other\nparameters.\n\n### An example\n\n```nginx\nupstream  uhost {\n    server                   s1;\n    server                   s2;\n    server                   s3 backup;\n    server                   s4;\n    # build singlet upstreams uhost_single_01,\n    # uhost_single_02, uhost_single_03 and uhost_single_04\n    combine_server_singlets  _single_ 2;\n    server                   s5;\n}\n```\n\n### Why numbers, not names?\n\nIn the example above, singlet upstreams will have names like *uhost_single_01*,\nbut names that contain server names like *uhost_single_s1* would look better and\nmore convenient. Why not use them instead ordering numbers? Unfortunately, Nginx\ndoes not remember server names after a server has been added into an upstream,\ntherefore we cannot simply fetch them.\n\n*Update.* There is a good news! Since version *1.7.2*, Nginx remembers server\nnames in upstream data and now we can use them when referring to a special\nkeyword *byname*. For example,\n\n```nginx\n    combine_server_singlets  byname;\n    # or\n    combine_server_singlets  _single_ byname;\n```\n\nAll colons (*:*) in the server names get replaced with underscores (*_*).\n\n### Where this can be useful\n\nA singlet upstream acts like a single server with fallback mode. This can be\nused to manage sticky HTTP sessions when backend servers identify themselves\nwith a proper mechanism such as HTTP cookies.\n\n```nginx\nupstream  uhost {\n    server  s1;\n    server  s2;\n    combine_server_singlets;\n}\n\nserver {\n    listen       8010;\n    server_name  main;\n    location / {\n        proxy_pass http://uhost$cookie_rt;\n    }\n}\nserver {\n    listen       8020;\n    server_name  server1;\n    location / {\n        add_header Set-Cookie \"rt=1\";\n        echo \"Passed to $server_name\";\n    }\n}\nserver {\n    listen       8030;\n    server_name  server2;\n    location / {\n        add_header Set-Cookie \"rt=2\";\n        echo \"Passed to $server_name\";\n    }\n}\n```\n\nIn this configuration, the first client request will choose backend server\nrandomly, the chosen server will set cookie *rt* to a predefined value (*1* or\n*2*), and all further requests from this client will be proxied to the chosen\nserver automatically until it goes down. Say, it was *server1*, then when it\ngoes down, the cookie *rt* on the client side will still be *1*. Directive\n*proxy_pass* will route the next client request to a singlet upstream *uhost1*\nwhere *server1* is declared active and *server2* is backed up. As soon as\n*server1* is not reachable any longer, Nginx will route the request to *server2*\nwhich will rewrite the cookie *rt* and all further client requests will be\nproxied to *server2* until it goes down.\n\nDirective extend_single_peers\n-----------------------------\n\nPeers in upstreams fail according to the rules listed in directive\n*proxy_next_upstream*. If an upstream has only one peer in its main or backup\npart then this peer will never fail. This can be a serious problem when writing\na custom algorithm for active health checks of upstream peers. Directive\n*extend_single_peers*, being declared in an upstream block, adds a fake peer\nmarked as *down* in the main or the backup part of the upstream if the part\noriginally contains only one peer. This makes Nginx mark the original single\npeer as failed when it fails to pass the rules of *proxy_next_upstream* just\nlike in the general case of multiple peers.\n\n### An example\n\n```nginx\nupstream  upstream1 {\n    server  s1;\n    extend_single_peers;\n}\n\nupstream  upstream2 {\n    server  s1;\n    server  s2;\n    server  s3 backup;\n    extend_single_peers;\n}\n```\n\nNotice that if a part (the main or the backup) of an upstream contains more than\none peer (like the main part in *upstream2* from the example) then the directive\nhas no effect: particularly, in the *upstream2* it only affects the backup part\nof the upstream.\n\nBlock upstrand\n--------------\n\nIs aimed to configure a super-layer of upstreams that do not lose their\nidentities. Accepts a number of directives including *upstream*, *order*,\n*next_upstream_statuses* and others. Upstreams with names starting with tilde\n(*~*) match a regular expression. Only upstreams that already have been declared\nbefore the upstrand block definition are regarded as candidates.\n\n### An example\n\n```nginx\nupstrand us1 {\n    upstream ~^u0 blacklist_interval=60s;\n    upstream b01 backup;\n    order start_random;\n    next_upstream_statuses error timeout non_idempotent 204 5xx;\n    next_upstream_timeout 60s;\n    intercept_statuses 5xx /Internal/failover;\n}\n```\n\nUpstrand *us1* will combine all upstreams whose names start with *u0* and\nupstream *b01* as backup. Backup upstreams are checked if all normal upstreams\nfail. The *failure* means that all upstreams in normal or backup cycles have\nresponded with statuses listed in directive *next_upstream_statuses* or been\n*blacklisted*. Here, the *upstream's response* means the status returned by the\nlast server of the upstream, which is strongly affected by value of directive\n*proxy_next_upstream*. An upstream is set as blacklisted when it has parameter\n*blacklist_interval* and responds with a status listed in the\n*next_upstream_statuses*. Blacklisting state is not shared between Nginx worker\nprocesses.\n\nThe next four upstrand directives are akin to those from the Nginx proxy module.\n\nDirective *next_upstream_statuses* accepts *4xx* and *5xx* statuses notation and\nvalues *error* and *timeout* to distinguish between cases when errors happen\nwith the upstream's peer connections from those when backends send statuses\n*502* or *504* (plain values *502* and *504* as well as *5xx* refer to both\ncases). It also accepts value *non_idempotent* to allow further processing of\n*non-idempotent* requests when they were responded by the last server from an\nupstream but failed according to other statuses listed in the directive.\nRequests are considered to be non-idempotent when their methods are *POST*,\n*LOCK* or *PATCH* just like in directive *proxy_next_upstream*.\n\nDirective *next_upstream_timeout* limits the overall duration time the upstrand\ncycles through all of its upstreams. If the time elapses while the upstrand is\nready to pass to a next upstream, the last upstream cycle result is returned.\n\nDirective *intercept_statuses* allows *upstrand failover* by intercepting the\nfinal response in location that matches the given URI. Interceptions must happen\neven when the upstrand times out. Notice also that walking through upstreams in\nan upstrand and the upstrand failover URI are not interceptable. Speaking more\ngenerally, any internal redirection (by *error_page*, *proxy_intercept_errors*,\n*X-Accel-Redirect* etc.) will break nested subrequests on which the upstrand's\nimplementation is based which leads to returning empty responses. These are\nextremely bad cases, and this is why walking through upstreams was protected\nagainst interceptions. The upstrand failover URI is more affected by this as\nthe implementation has less control over its location. Particularly, the\nupstrand failover has only protection against interceptions by *error_page* and\n*proxy_intercept_errors*. This means that the upstrand failover URI location\nmust be as simple as possible (e.g. using simple directives like *return* or\n*echo*).\n\nThat said, there is a decent solution to the problem with upstrand failover\nlocations and internal redirections in them. How exactly do internal\nredirections *break* subrequests? Well, they *erase* subrequest contexts needed\nin response filters of the module. So, if we could make subrequest context\npersistent, would we solve the problem? The answer is yes! Nginx module\n[nginx-easy-context](https://github.com/lyokha/nginx-easy-context) enables\nbuilding persistent request contexts. Upstrands can benefit from them by turning\non a switch in file *config* and building both modules. See details in section\n[Build and test](#build-and-test).\n\nDirective *order* currently accepts only one value *start_random* which means\nthat starting upstreams in normal and backup cycles after worker fired up will\nbe chosen randomly. Starting upstreams in further requests will be cycled in\nround-robin manner. Additionally, a modifier *per_request* is also accepted in\nthe *order* directive: it turns off the global per-worker round-robin cycle.\nThe combination of *per_request* and *start_random* makes the starting upstream\nin every new request be chosen randomly.\n\nSuch a failover between *failure* statuses can be reached during a single\nrequest by feeding a special variable that starts with *upstrand_* to the\n*proxy_pass* directive like so:\n\n```nginx\nlocation /us1 {\n    proxy_pass http://$upstrand_us1;\n}\n```\n\nBe careful when accessing this variable from other directives! It starts up the\nsubrequests machinery which may be not desirable in many cases.\n\n### Upstrand status variables\n\nThere are a number of upstrand status variables available: *upstrand_addr*,\n*upstrand_cache_status*, *upstrand_connect_time*, *upstrand_header_time*,\n*upstrand_response_length*, *upstrand_response_time* and *upstrand_status*. They\nall are counterparts of corresponding *upstream* variables and contain the\nvalues of the latter for all upstreams passed through a request and all\nsubrequests chronologically. Variable *upstrand_path* contains path of all\nupstreams visited during request.\n\n### Where this can be useful\n\nThe *upstrand* looks very similar to a simple combined upstream but it also has\na crucial difference: the upstreams inside of an upstrand do not get flattened\nand keep holding their identities. This gives a possibility to configure a\n*failover* status for a group of servers associated with a single upstream\nwithout need to check them all by turn. In the above example, upstrand *us1* may\nhold a list of upstreams like *u01*, *u02* etc. Imagine that upstream *u01*\nholds 10 servers inside and represents a part of a geographically distributed\nbackend system. Let upstrand *us1* combine all such parts in a whole, and let us\nrun a client application that polls the parts for doing some tasks. Let the\nbackends send HTTP status *204* if they do not have new tasks. In a flat\ncombined upstream, all 10 servers may have been polled before the application\nwill finally receive a new task from another upstream. The upstrand *us1* allows\nskipping to the next upstream after checking the first server in an upstream\nthat does not have tasks. This machinery is apparently suitable for *upstream\nbroadcasting*, when messages are being sent to all upstreams in an upstrand.\n\nThe examples above show that an upstrand can be regarded as a *2-dimensional*\nupstream that comprises a number of clusters representing natural upstreams and\nallows short-cycling over them.\n\nTo illustrate this, let's emulate an upstream without round-robin balancing.\nEvery new client request will start by proxying to the first server in the\nupstream list and then failing over to the next server.\n\n```nginx\n    upstream u1 {\n        server localhost:8020;\n        server localhost:8030;\n        combine_server_singlets _single_ nobackup;\n    }\n\n    upstrand us1 {\n        upstream ~^u1_single_ blacklist_interval=60s;\n        order per_request;\n        next_upstream_statuses error timeout non_idempotent 5xx;\n        intercept_statuses 5xx /Internal/failover;\n    }\n```\n\nDirective *combine_server_singlets* in upstream *u1* generates two singlet\nupstreams *u1_single_1* and *u1_single_2* to inhabit upstrand *us1*. Due to\n*per_request* ordering inside the upstrand, the two upstreams will be traversed\nin order *u1_single_1 → u1_single_2* in each client request.\n\nDirective dynamic_upstrand\n--------------------------\n\nAllows choosing an upstrand from passed variables in run-time. The directive can\nbe set in server, location and location-if clauses.\n\nIn the following configuration\n\n```nginx\n    upstrand us1 {\n        upstream ~^u0;\n        upstream b01 backup;\n        order start_random;\n        next_upstream_statuses 5xx;\n    }\n    upstrand us2 {\n        upstream ~^u0;\n        upstream b02 backup;\n        order start_random;\n        next_upstream_statuses 5xx;\n    }\n\n    server {\n        listen       8010;\n        server_name  main;\n\n        dynamic_upstrand $dus1 $arg_a us2;\n\n        location / {\n            dynamic_upstrand $dus2 $arg_b;\n            if ($arg_b) {\n                proxy_pass http://$dus2;\n                break;\n            }\n            proxy_pass http://$dus1;\n        }\n    }\n```\n\nupstrands returned in variables *dus1* and *dus2* are to be chosen from values\nof variables *arg_a* and *arg_b*. If *arg_b* is set then the client request will\nbe sent to an upstrand with name equal to the value of *arg_b*. If there is not\nan upstrand with this name then *dus2* will be empty and *proxy_pass* will\nreturn HTTP status *500*. To prevent initialization of a dynamic upstrand\nvariable with empty value, its declaration must be terminated with a literal\nname that corresponds to an existing upstrand. In this example, dynamic upstrand\nvariable *dus1* will be initialized by the upstrand *us2* if *arg_a* is empty or\nnot set. Altogether, if *arg_b* is not set or empty and *arg_a* is set and has a\nvalue equal to an existing upstrand, the request will be sent to this upstrand,\notherwise (if *arg_b* is not set or empty and *arg_a* is set but does not refer\nto an existing upstrand) *proxy_pass* will most likely return HTTP status *500*\n(except there is a variable composed from literal string *upstrand_* and the\nvalue of *arg_a* that points to a valid destination), otherwise (both *arg_b*\nand *arg_a* are not set or empty) the request will be sent to the upstrand\n*us2*.\n\nBuild and test\n--------------\n\nThe module is built with the standard Nginx build approach from the directory\nwith Nginx source files. If you want to link this module with the Nginx\nexecutable file statically, use *configure* option *--add-module*, e.g.\n\n```ShellSession\n$ ./configure --add-module=/path/to/this/module\n$ make\n$ sudo make install\n```\n\nTo use the module as a dynamic library, choose option *--add-dynamic-module*.\n\n```ShellSession\n$ ./configure --add-dynamic-module=/path/to/this/module\n$ make\n$ sudo make install\n```\n\nIn the latter case, put directive\n\n```nginx\nload_module modules/ngx_http_combined_upstreams_module.so\n```\n\nin the Nginx configuration file.\n\nTo benefit from persistent request contexts and upstrand failover locations with\ninternal redirections in them (*try_files*, *error_page* etc.), download module\n[nginx-easy-context](https://github.com/lyokha/nginx-easy-context) in some\ndirectory, set environment variable\n*NGX_HTTP_COMBINED_UPSTREAMS_PERSISTENT_UPSTRAND_INTERCEPT_CTX* to *y* or *yes*,\nand run *configure* with two options *--add-module*.\n\n```ShellSession\n$ NGX_HTTP_COMBINED_UPSTREAMS_PERSISTENT_UPSTRAND_INTERCEPT_CTX=yes\n$ ./configure --add-module=/path/to/module/nginx-easy-context --add-module=/path/to/this/module\n```\n\nThe order of the two options matters. Module *nginx-easy-context* must go first.\n\nWith command *prove* from Perl module *Test::Harness* and Perl module\n*Test::Nginx::Socket*, tests can be run by a regular user from directory\n*test/*.\n\n```ShellSession\n$ prove -r t\n```\n\nAdd option *-v* for verbose output. Before run, you may need to adjust\nenvironment variable *PATH* to point to the Nginx installation directory.\n\nSee also\n--------\n\nThere are several articles about the module in my blog, in chronological order:\n\n1. [*Простой модуль nginx для создания комбинированных\nапстримов*](http://lin-techdet.blogspot.com/2011/10/nginx.html) (in Russian). A\ncomprehensive article discovering details of implementation of directive\n*add_upstream* which can also be regarded as a small tutorial for Nginx modules\ndevelopment.\n2. [*nginx upstrand to configure super-layers of\nupstreams*](http://lin-techdet.blogspot.com/2015/09/nginx-upstrand-to-configure-super.html).\nAn overview of block *upstrand* usage and some details on its implementation.\n3. [*Не такой уж простой модуль nginx для создания комбинированных\nапстримов*](http://lin-techdet.blogspot.com/2015/12/nginx.html) (in Russian). An\noverview of all features of the module with configuration examples and testing\nsession samples.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyokha%2Fnginx-combined-upstreams-module","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flyokha%2Fnginx-combined-upstreams-module","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyokha%2Fnginx-combined-upstreams-module/lists"}