Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nginx sticky annotations #258

Merged
merged 8 commits into from
Feb 16, 2017

Conversation

rikatz
Copy link
Contributor

@rikatz rikatz commented Feb 10, 2017

This PR adds support for configuring the 'sticky' directive per Ingress, with annotations, instead of enabling or disabling it globally through ConfigMap.

Also it adds support for configuring the name of the cookie that will be used to route the requests, and the hash that will be used to generate the cookie.

In the future, as this is a per-Ingress feature, I think it would be better to disable and remove this configuration from ConfigMap and keep it only per Ingress.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Feb 10, 2017
@k8s-reviewable
Copy link

This change is Reviewable

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 46.145% when pulling 6809319 on rikatz:nginx-sticky-annotations into 01c4301 on kubernetes:master.

@cmluciano
Copy link
Contributor

related #259

@@ -0,0 +1,58 @@
# Sticky Session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest a directory structure like

examples/
  affinity/
    cookie/
      nginx/
      haproxy/
    client
      nginx/
      gce/
        ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,58 @@
# Sticky Session

This example demonstrates how to Stickness in a Ingress.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest: how to achieve session affinity using cookies
Though unlike haproxy we're not allowed to pick the cookie right? nginx just uses a hash of the request IIRC

Copy link
Contributor Author

@rikatz rikatz Feb 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, actually this session affinity is through cookie. The sticky module captures the cookie, and routes the request based on this. I didn't deployed more than one NGINX to check if the hash that the cookie assigns keeps the same, between them, but I think it did, as the hash is based on the upstream name.

But I will change the description as suggested.

controller by specifying the [ingress.class annotation](/examples/PREREQUISITES.md#ingress-class),
and that you have an ingress controller [running](/examples/deployment) in your cluster.

Also, you need to have a deployment with replica > 1. Using a deployment with only one replica doesn't set the 'sticky' cookie.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest: you will also need to deploy multiple replicas of your application that show up as endpoints for the Service referenced in the Ingress object, to test session stickyness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you need to have a deployment with replica > 1. Using a deployment with only one replica doesn't set the 'sticky' cookie.

## Deployment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest something like:

Session stickyness is achieved through 3 annotations on the Ingress, as shown in the example.

Name Description Values
ingress.kubernetes.io/sticky-enabled annotation enables stickyness true/false
ingress.kubernetes.io/sticky-name (does what?) route, (and what are the possible values)
ingress.kubernetes.io/sticky-hash does what? sha1 (does what, and what are the possible values)

You can create the ingress to test this

$ kubectl create -f 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7s 7s 1 {nginx-ingress-controller } Normal CREATE default/nginx-test


$ curl -I http://stickyingress.example.com
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do they need DNS name or will IPs suffice?
Suggest using only IPs if that'll do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to use IP, as this is a per vhost configuration

)

const (
stickyEnabled = "ingress.kubernetes.io/sticky-enabled"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need an enabled? Can we achieve this with a single annotation like: ingress.kubernetes.io/affinity=cookie (defaults cookie and hash, unless they're specified, and if affinity is set to eg: client, then hash and cookie values are ignored)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bprashanth You mean only one annotation, with ingress.kubernetes.io/affinity=cookie,hash=md5,name=route ?


const (
stickyEnabled = "ingress.kubernetes.io/sticky-enabled"
stickyName = "ingress.kubernetes.io/sticky-name"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename to session-cookie-name and leave a comment like:
// If a cookie with this name exists, its value is used as an index into the list of available backends.

const (
stickyEnabled = "ingress.kubernetes.io/sticky-enabled"
stickyName = "ingress.kubernetes.io/sticky-name"
stickyHash = "ingress.kubernetes.io/sticky-hash"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename to session-cookie-hash-algorithm
// This is the algorithm used by nginx to generate a value for the session cookie, if
// one isn't supplied and affintiy is set to "cookie".

@@ -177,6 +180,20 @@ To configure this setting globally for all Ingress rules, the `whitelist-source-
Please check the [whitelist](examples/whitelist/README.md) example.


### Sticky Session

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link to the example from here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Set-Cookie: route=a9907b79b248140b56bb13723f72b67697baac3d; Path=/; HttpOnly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please mention that if the cookies is supplied by a user, it is honored (right? but what if it's some random string or int?), but if it isn't supplied, nginx will generate one and the client is expected to remember this value and supply it on the next request to map to the same backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is the behaviour of any sticky session cookie (also when you use them in Apache with mod_proxy). If the user changes the cookie, it looses the affinity and a new one is generated.

@rikatz
Copy link
Contributor Author

rikatz commented Feb 12, 2017

@bprashanth Made all the reviews, but couldn't test yet. Will test tomorrow at my corporate environment.

If you could, anyway take a look in the changes to see if I've missed something (but no merging yet) I would appreciate.

Thanks :)

@coveralls
Copy link

Coverage Status

Coverage increased (+0.3%) to 46.267% when pulling a158e5f on rikatz:nginx-sticky-annotations into 01c4301 on kubernetes:master.

@bprashanth
Copy link
Contributor

@jaykeshur

Copy link
Contributor

@bprashanth bprashanth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM sans nits


Session stickyness is achieved through 3 annotations on the Ingress, as shown in the [example](sticky-ingress.yaml).

|Name|Description|Values|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't showing up as a table, I guess you need a little more formatting: https://help.github.com/articles/organizing-information-with-tables/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, corrected it

// If a cookie with this name exists,
// its value is used as an index into the list of available backends.
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
defaultAffinityCookieName = "route"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better default name still makes sense, as suggested here #258 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default name used by nginx sticky module. I don't think using SESSIONID or BACKEND a good idea, as this might also be a bit confusing with something related to the app.

But INGRESSCOOKIE is something we can use here.

Also, there is no guideline (I couldn't find) of best practices when setting a Custom HTTP Header. Amazon uses them all as Lower case. Google uses them all Upper case.

I will make this change here, just in case.

type AffinityConfig struct {
// The type of affinity that will be used
AffinityType string `json:"type"`
CookieAffinityConfig CookieAffinityConfig `json:"cookieconfig"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change the name of the field to "Cookie" and make this a pointer. See the pattern here: https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/extensions/types.go#L642

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this colide with the requested here: #258 (comment)

Also, isn't a better naming convention using something like 'CookieConfig'? Maybe changing this to 'Cookie' only can cause the impression that he have here something that manages cookies at all, that could lead to some misunderstanding of the code, right?

// AffinityConfig describes the per ingress session affinity config
type AffinityConfig struct {
// The type of affinity that will be used
AffinityType string `json:"type"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with including the type as a field since this is an internal api, but we don't really need a string indicating the type when we can just check for a field with the same name being non-nil right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking in something like HAProxy.

You can have affinity per IP, or per cookie, and this makes other configurable parameters different, like described here

So I think it's fine to have the selected type, as you can take actions based on it's value.

Also, in nginx.tmpl (as an example), we only have 'cookie' type. So if I only check for nil values, I could misconfigure my ingress, like setting a value like 'affinity: per-ip' and thinking it would work in NGINX ingress

if err != nil {
at = ""
}
//cookieAffinityConfig = CookieAffinityParse(ing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you keeping this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot that when was debugging the code. Just removed it

}
//cookieAffinityConfig = CookieAffinityParse(ing)
switch at {
case "cookie":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other places we just check for non-nil: https://github.com/kubernetes/kubernetes/blob/master/pkg/api/validation/validation.go#L346, but I'm fine with the way you have it right now.

However, please define the string "cookie" as a const, i.e somewhere, probably types.go, put

type Affinity string

const (
  Cookie Affinity = "cookie"
  Client Affinity = "client"
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bprashanth you mean a new types.go inside the 'sessionaffinity' directory where we can put every constant, or directly in 'core/pkg/ingress/types.go' ?

Hash string `json:"hash"`
}

type affinity struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please define this under NewParser but above (a affinity) Parse

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

name: nginx-test
annotations:
kubernetes.io/ingress.class: "nginx"
ingress.kubernetes.io/affinity: "cookie"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So re: my previous comment about one annotation, if just ingress.kubernetes.io/affinity is supplied and nothing else, we still get sticky with default alg and default cookie name correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. If you supply only affinity=cookie, the default alg and cookie name will be the set here

@bprashanth
Copy link
Contributor

bprashanth commented Feb 14, 2017

Only question I have is about the default name of the cookie, and if we can pick one better than "route"
EDIT: Didn't notice you changed it to INGRESSCOOKIE, only one nit about that name :)

Travis failure looks legit

# k8s.io/ingress/core/pkg/ingress/annotations/sessionaffinity
core/pkg/ingress/annotations/sessionaffinity/main_test.go:81: nginxAffinity.CookieAffinityConfig undefined (type *AffinityConfig has no field or method CookieAffinityConfig)
core/pkg/ingress/annotations/sessionaffinity/main_test.go:82: nginxAffinity.CookieAffinityConfig undefined (type *AffinityConfig has no field or method CookieAffinityConfig)
core/pkg/ingress/annotations/sessionaffinity/main_test.go:85: nginxAffinity.CookieAffinityConfig undefined (type *AffinityConfig has no field or method CookieAffinityConfig)
core/pkg/ingress/annotations/sessionaffinity/main_test.go:86: nginxAffinity.CookieAffinityConfig undefined (type *AffinityConfig has no field or method CookieAffinityConfig)

@@ -31,7 +31,7 @@ const (
// If a cookie with this name exists,
// its value is used as an index into the list of available backends.
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
defaultAffinityCookieName = "route"
defaultAffinityCookieName = "INGRESSCOOKIE"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a naming convention we can follow for HTTP headers? ALL CAPS, or first Cap, or X-Blah, would be great if you could dig up some doc or rfc and reference it here, but otherwise just a simple comment about the CAPS is ok

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bprashanth I've got this, but no reference to sticky / cookie session:

https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

I've seen in there also that it's common to use something like CamelCase (IngressCookie) as an example.

Headers containing X- are going to be deprecated, as the following RFC:

https://tools.ietf.org/html/rfc6648

So we can keep this as all caps by now (as does Google), and reopen this discussion later?

The RFC section that deals with Requests and Responses Headers is the following: https://tools.ietf.org/html/rfc2616#section-5.3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, whatever makes sense. Something like this will be hard to change without a deprecation notice, because people will probably have clients written to it, but I suppose they can always set the cookie name explicitly back to INGRESSCOOKIE via annotation.
Google uses all caps? for what?

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 46.199% when pulling b06ead1 on rikatz:nginx-sticky-annotations into 01c4301 on kubernetes:master.

@fanhaozzu
Copy link

so cool!
@bprashanth , the pull request should be merge

@rikatz
Copy link
Contributor Author

rikatz commented Feb 15, 2017

@bprashanth Please don't merge yet. Couldn't test the behaviour here in my environment (after all the changes). Will do this tomorrow and report you.

Is there any other think I need to change by now?

Tks for the hard work!

@bprashanth
Copy link
Contributor

Cool, will wait for you to ack. LGTM otherwise.

@@ -51,6 +51,9 @@ The following annotations are supported:
|[ingress.kubernetes.io/upstream-max-fails](#custom-nginx-upstream-checks)|number|
|[ingress.kubernetes.io/upstream-fail-timeout](#custom-nginx-upstream-checks)|number|
|[ingress.kubernetes.io/whitelist-source-range](#whitelist-source-range)|CIDR|
|[ingress.kubernetes.io/sticky-enabled](#sticky-session)|true or false|
|[ingress.kubernetes.io/sticky-name](#sticky-session)|string|
|[ingress.kubernetes.io/sticky-hash](#sticky-session)|string|

Copy link

@fanhaozzu fanhaozzu Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suggest add ingress.kubernetes.io/sticky-expire to control cookie expire time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fanhaozzu Agreed. I just think that by now we could merge this, and add this feature later (we can open a new issue for this one).

This is because this item would need some more work to validate it's input, as this is a time pattern. Maybe using this

Can we work in this item latelly?

@rikatz
Copy link
Contributor Author

rikatz commented Feb 16, 2017

@bprashanth Some considerations after the tests:

  1. It works fine. The workflow about multiple instances of ingress sharing the same cookie hash works. I've tested agains multiple nginx ingress controllers, the cookie that was set by the first made the other ingresses route through the same backend/upstream.

  2. When I delete a pod from a service (simulating the pod failure), the cookie is regenerated with the hash of the other picked up backend. And keeps the affinity with that new backend.

  3. When you point different ingress objects to the same service name, one containing affinity configuration and the other don't, the first created ingress is respected. So it creates the 'upstream' of the first ingress controller. If it doesn't contain any affinity configuration, the upstream is created without it.

With these observations made, I think it's fine to merge this PR to Master.

@fanhaozzu do you mind opening that new issue with your request of setting also the cookie expiration? I'll work on that ASAP (or you can take that and also make your very own PR)

Thanks guys.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 46.237% when pulling 4be502e on rikatz:nginx-sticky-annotations into 01c4301 on kubernetes:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 45.945% when pulling 4be502e on rikatz:nginx-sticky-annotations into 01c4301 on kubernetes:master.

@bprashanth
Copy link
Contributor

Cool, @rikatz thanks for the pr, merging

@bprashanth bprashanth merged commit 698c084 into kubernetes:master Feb 16, 2017
arjanschaaf pushed a commit to arjanschaaf/ingress that referenced this pull request Apr 21, 2017
aledbf added a commit that referenced this pull request Apr 21, 2017
Nginx sticky annotations #258 made the global enable-sticky-sessions obsolete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants