After going through the nginx-sticky-module and understanding the requirements for session based load balancing, we were convinced that the cookie based approach implemented earlier had several drawbacks, including -
1. The most obvious one, as stated on the nginx-sticky-module page itself, is that it requires cookies to be enabled by the browser. If that fails, the load balancing either falls back to the regular round robin mechanism or returns a bad gateway error depending on its configuration.
2. This cookie based approach isn't completely transparent. To ensure persistence, both the client and server are involved, they are both aware of the cookie being stored on the client side. If there is a server side processing simply based on the presence of a cookie in the incoming request, the logic fails as the load balancer intervenes to add another cookie. So yea, not completely transparent!
3. The third and more significant drawback, is the session timeout. At the application level, the session may last for a few minutes or more, there might also be an upper limit on the duration of the session. On the other hand, the cookie being stored for persistence has an associated lifetime, which will definitely be different from the actual user sessions, usually set to the length of the longest session by the developer! As mentioned in our previous blog, this will cause the load balancing to get skewed, causing the requests to go the the same server even after the user session has expired.
4. Another drawback of this module, is that the client side gets too much control. This is the consequence of the transparency problem mentioned earlier. Since the client gets the digest of the server ip in the cookie, the client can track all the sever side mappings, by simply sending a cookie free request each time - the nginx load balancer would allot a different server each time using the default round robin approach. The concept of a reverse proxy to abstract the backend servers is somewhat lost. The client can then use the information to flood one particular server with requests, succeeding in a DOS attack!
5. Https, by using TLS, encrypts all the data, including the headers. Only the hostname isn't encrypted.
For instance : the first part of the URL (https://www.google.com) is still visible as it builds the connection. The second part (/herearemygetparameters/1/2/3/4) is protected by TLS. So if the application uses https and if the termination proxy is located behind the load balancer, this approach fails as the cookies become inaccessible.
After identifying the above shortcomings, we wanted to be able to counter as many of them in our approach, if not all, using query parameters.
Our approach involves the use of 2 modules -
1. The first module is a load balancer, similar to the nginx-sticky-module using cookies. The main idea is to fetch the query parameters from the incoming url request. Extract the session id, which is the unique identifier for a client, from the query parameters. This is then matched against our own Hash Map which stores the mappings of the session id (key) and the server id(value). If a match is found, the client's request is redirected to the appropriate server. If there are no query parameters, or if there is no session id in the query parameters, or if the session id does not match with any in the hash map, a server is selected in round robin fashion.
For a new request (or otherwise) , if a new session id is assigned by the server, it has to be mapped to the server id. This can be done only after the response message has been built, clearly indicating the need of a second module to process the response sent by the server.
2. The second module, a filter, parses the response body to look for a new session id. This parsing can either involve :
a. Parsing only for form action urls and examining the query parameters contained in them (to add them to the HashMap). This approach is restrictive and only addresses form based requests.
b. Parsing for all urls and only storing the session id information from those which belong to the same domain.
As the number of user sessions increase, there is also a need to clean up some of the old mappings to optimize space. This wasn't a necessity in case of the cookie based approach as the the number of mappings was equal to the number of the backend servers in the system. With our approach, the number of mappings keep increasing with new user sessions, hence the filter module also has to clear some of the mappings after a configured timeout. For the same reason, we decided to associate a new mapping with a time attribute as well.
This alternative approach for sticky sessions counters some of the drawbacks of the cookie based approach.
- It doesn't rely on the client's browser to enable cookies.
- It is transparent on the client's side, we use a cookies only to share information across modules, before and after the server's response.
- The application is in control of the timeout, the requests are sent to the same server so long as the session id is the same in the incoming request and within the timeout. The onus of the session id is on the application, if the server session has expired, then the server will no longer generate a response with the same session id.
- The client can still access the session id which is used as a unique identifier and can flood by sending many requests with the same session id. But we cannot control the server to which the requests are sent like in the case of the sticky module.
- Lastly, since this method relies on parsing the query parameters as well as the response body, https is still a limitation if the termination proxy lies beyond the load balancer, as only the hostname is not encrypted.
After going through the nginx-sticky-module and understanding the requirements for session based load balancing, we were convinced that the cookie based approach implemented earlier had several drawbacks, including -
1. The most obvious one, as stated on the nginx-sticky-module page itself, is that it requires cookies to be enabled by the browser. If that fails, the load balancing either falls back to the regular round robin mechanism or returns a bad gateway error depending on its configuration.
2. This cookie based approach isn't completely transparent. To ensure persistence, both the client and server are involved, they are both aware of the cookie being stored on the client side. If there is a server side processing simply based on the presence of a cookie in the incoming request, the logic fails as the load balancer intervenes to add another cookie. So yea, not completely transparent!
3. The third and more significant drawback, is the session timeout. At the application level, the session may last for a few minutes or more, there might also be an upper limit on the duration of the session. On the other hand, the cookie being stored for persistence has an associated lifetime, which will definitely be different from the actual user sessions, usually set to the length of the longest session by the developer! As mentioned in our previous blog, this will cause the load balancing to get skewed, causing the requests to go the the same server even after the user session has expired.
4. Another drawback of this module, is that the client side gets too much control. This is the consequence of the transparency problem mentioned earlier. Since the client gets the digest of the server ip in the cookie, the client can track all the sever side mappings, by simply sending a cookie free request each time - the nginx load balancer would allot a different server each time using the default round robin approach. The concept of a reverse proxy to abstract the backend servers is somewhat lost. The client can then use the information to flood one particular server with requests, succeeding in a DOS attack!
5. Https, by using TLS, encrypts all the data, including the headers. Only the hostname isn't encrypted.
For instance : the first part of the URL (https://www.google.com) is still visible as it builds the connection. The second part (/herearemygetparameters/1/2/3/4) is protected by TLS. So if the application uses https and if the termination proxy is located behind the load balancer, this approach fails as the cookies become inaccessible.
After identifying the above shortcomings, we wanted to be able to counter as many of them in our approach, if not all, using query parameters.
Our approach involves the use of 2 modules -
1. The first module is a load balancer, similar to the nginx-sticky-module using cookies. The main idea is to fetch the query parameters from the incoming url request. Extract the session id, which is the unique identifier for a client, from the query parameters. This is then matched against our own Hash Map which stores the mappings of the session id (key) and the server id(value). If a match is found, the client's request is redirected to the appropriate server. If there are no query parameters, or if there is no session id in the query parameters, or if the session id does not match with any in the hash map, a server is selected in round robin fashion.
For a new request (or otherwise) , if a new session id is assigned by the server, it has to be mapped to the server id. This can be done only after the response message has been built, clearly indicating the need of a second module to process the response sent by the server.
2. The second module, a filter, parses the response body to look for a new session id. This parsing can either involve :
a. Parsing only for form action urls and examining the query parameters contained in them (to add them to the HashMap). This approach is restrictive and only addresses form based requests.
b. Parsing for all urls and only storing the session id information from those which belong to the same domain.
As the number of user sessions increase, there is also a need to clean up some of the old mappings to optimize space. This wasn't a necessity in case of the cookie based approach as the the number of mappings was equal to the number of the backend servers in the system. With our approach, the number of mappings keep increasing with new user sessions, hence the filter module also has to clear some of the mappings after a configured timeout. For the same reason, we decided to associate a new mapping with a time attribute as well.
This alternative approach for sticky sessions counters some of the drawbacks of the cookie based approach.
- It doesn't rely on the client's browser to enable cookies.
- It is transparent on the client's side, we use a cookies only to share information across modules, before and after the server's response.
- The application is in control of the timeout, the requests are sent to the same server so long as the session id is the same in the incoming request and within the timeout. The onus of the session id is on the application, if the server session has expired, then the server will no longer generate a response with the same session id.
- The client can still access the session id which is used as a unique identifier and can flood by sending many requests with the same session id. But we cannot control the server to which the requests are sent like in the case of the sticky module.
- Lastly, since this method relies on parsing the query parameters as well as the response body, https is still a limitation if the termination proxy lies beyond the load balancer, as only the hostname is not encrypted.