Skip to content

Commit

Permalink
Improve the session affinity feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricardo Pchevuzinske Katz committed Feb 12, 2017
1 parent 6809319 commit a158e5f
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 213 deletions.
13 changes: 7 additions & 6 deletions controllers/nginx/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,21 @@ To configure this setting globally for all Ingress rules, the `whitelist-source-

*Note:* Adding an annotation to an Ingress rule overrides any global restriction.

Please check the [whitelist](examples/whitelist/README.md) example.
Please check the [whitelist](examples/affinity/cookie/nginx/README.md) example.


### Sticky Session

The annotation `ingress.kubernetes.io/sticky-enabled` enables stickness in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server.
The annotation `ingress.kubernetes.io/affinity` enables and sets the affinity type in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server.

You can also specify the name of the cookie that will be used to route the requests with the annotation `ingress.kubernetes.io/sticky-name`. The default is to create a cookie named 'route'.

The annotation `ingress.kubernetes.io/sticky-hash` defines which algorithm will be used to 'hash' the used upstream. Default value is `md5` and possible values are `md5`, `sha1` and `index`.
#### Cookie affinity
If you use the ``cookie`` type you can also specify the name of the cookie that will be used to route the requests with the annotation `ingress.kubernetes.io/session-cookie-name`. The default is to create a cookie named 'route'.

This feature is implemented by the third party module *nginx-sticky-module-ng* (https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng).
In case of NGINX the annotation `ingress.kubernetes.io/session-cookie-hash` defines which algorithm will be used to 'hash' the used upstream. Default value is `md5` and possible values are `md5`, `sha1` and `index`.
The `index` option is not hashed, an in-memory index is used instead, it's quicker and the overhead is shorter Warning: the matching against upstream servers list is inconsistent. So, at reload, if upstreams servers has changed, index values are not guaranted to correspond to the same server as before! USE IT WITH CAUTION and only if you need to!

The workflow used to define which upstream server will be used is explained here: https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/raw/08a395c66e425540982c00482f55034e1fee67b6/docs/sticky.pdf
In NGINX this feature is implemented by the third party module [nginx-sticky-module-ng](https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng). The workflow used to define which upstream server will be used is explained [here]https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/raw/08a395c66e425540982c00482f55034e1fee67b6/docs/sticky.pdf



Expand Down
4 changes: 2 additions & 2 deletions controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ http {

{{range $name, $upstream := $backends}}
upstream {{$upstream.Name}} {
{{ if $upstream.StickySession.Enabled }}
sticky hash={{$upstream.StickySession.Hash}} name={{$upstream.StickySession.Name}} httponly;
{{ if eq $upstream.SessionAffinity.AffinityType "cookie" }}
sticky hash={{$upstream.SessionAffinity.CookieSessionAffinity.Hash}} name={{$upstream.SessionAffinity.CookieSessionAffinity.Name}} httponly;
{{ else }}
least_conn;
{{ end }}
Expand Down
118 changes: 118 additions & 0 deletions core/pkg/ingress/annotations/sessionaffinity/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sessionaffinity

import (
"regexp"

"github.com/golang/glog"

"k8s.io/kubernetes/pkg/apis/extensions"

"k8s.io/ingress/core/pkg/ingress/annotations/parser"
)

const (
annotationAffinityType = "ingress.kubernetes.io/affinity"
// 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"
// 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".
annotationAffinityCookieHash = "ingress.kubernetes.io/session-cookie-hash"
defaultAffinityCookieHash = "md5"
)

var (
affinityCookieHashRegex = regexp.MustCompile(`^(index|md5|sha1)$`)
)

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

// CookieAffinityConfig describes the Config of cookie type affinity
type CookieAffinityConfig struct {
// The name of the cookie that will be used in case of cookie affinity type.
Name string `json:"name"`
// The hash that will be used to encode the cookie in case of cookie affinity type
Hash string `json:"hash"`
}

type affinity struct {
}

// CookieAffinityParse gets the annotation values related to Cookie Affinity
// It also sets default values when no value or incorrect value is found
func CookieAffinityParse(ing *extensions.Ingress) *CookieAffinityConfig {

sn, err := parser.GetStringAnnotation(annotationAffinityCookieName, ing)

if err != nil || sn == "" {
glog.V(3).Infof("Ingress %v: No value found in annotation %v. Using the default %v", ing.Name, annotationAffinityCookieName, defaultAffinityCookieName)
sn = defaultAffinityCookieName
}

sh, err := parser.GetStringAnnotation(annotationAffinityCookieHash, ing)

if err != nil || !affinityCookieHashRegex.MatchString(sh) {
glog.V(3).Infof("Invalid or no annotation value found in Ingress %v: %v. Setting it to default %v", ing.Name, annotationAffinityCookieHash, defaultAffinityCookieHash)
sh = defaultAffinityCookieHash
}

return &CookieAffinityConfig{
Name: sn,
Hash: sh,
}
}

// NewParser creates a new Affinity annotation parser
func NewParser() parser.IngressAnnotation {
return affinity{}
}

// ParseAnnotations parses the annotations contained in the ingress
// rule used to configure the affinity directives
func (a affinity) Parse(ing *extensions.Ingress) (interface{}, error) {

var cookieAffinityConfig *CookieAffinityConfig
cookieAffinityConfig = &CookieAffinityConfig{}

// Check the type of affinity that will be used
at, err := parser.GetStringAnnotation(annotationAffinityType, ing)
if err != nil {
at = ""
}
//cookieAffinityConfig = CookieAffinityParse(ing)
switch at {
case "cookie":
cookieAffinityConfig = CookieAffinityParse(ing)

default:
glog.V(3).Infof("No default affinity was found for Ingress %v", ing.Name)

}
return &AffinityConfig{
AffinityType: at,
CookieAffinityConfig: *cookieAffinityConfig,
}, nil

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package stickysession
package sessionaffinity

import (
"testing"
Expand Down Expand Up @@ -59,30 +59,30 @@ func buildIngress() *extensions.Ingress {
}
}

func TestIngressStickySession(t *testing.T) {
func TestIngressAffinityCookieConfig(t *testing.T) {
ing := buildIngress()

data := map[string]string{}
data[stickyEnabled] = "true"
data[stickyHash] = "md5"
data[stickyName] = "route1"
data[annotationAffinityType] = "cookie"
data[annotationAffinityCookieHash] = "sha123"
data[annotationAffinityCookieName] = "route"
ing.SetAnnotations(data)

sti, _ := NewParser().Parse(ing)
nginxSti, ok := sti.(*StickyConfig)
affin, _ := NewParser().Parse(ing)
nginxAffinity, ok := affin.(*AffinityConfig)
if !ok {
t.Errorf("expected a StickyConfig type")
t.Errorf("expected a Config type")
}

if nginxSti.Hash != "md5" {
t.Errorf("expected md5 as sticky-hash but returned %v", nginxSti.Hash)
if nginxAffinity.AffinityType != "cookie" {
t.Errorf("expected cookie as sticky-type but returned %v", nginxAffinity.AffinityType)
}

if nginxSti.Hash != "md5" {
t.Errorf("expected md5 as sticky-hash but returned %v", nginxSti.Hash)
if nginxAffinity.CookieAffinityConfig.Hash != "md5" {
t.Errorf("expected md5 as sticky-hash but returned %v", nginxAffinity.CookieAffinityConfig.Hash)
}

if !nginxSti.Enabled {
t.Errorf("expected sticky-enabled but returned %v", nginxSti.Enabled)
if nginxAffinity.CookieAffinityConfig.Name != "route" {
t.Errorf("expected route as sticky-name but returned %v", nginxAffinity.CookieAffinityConfig.Name)
}
}
90 changes: 0 additions & 90 deletions core/pkg/ingress/annotations/stickysession/main.go

This file was deleted.

18 changes: 9 additions & 9 deletions core/pkg/ingress/controller/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import (
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/annotations/secureupstream"
"k8s.io/ingress/core/pkg/ingress/annotations/sessionaffinity"
"k8s.io/ingress/core/pkg/ingress/annotations/sslpassthrough"
"k8s.io/ingress/core/pkg/ingress/annotations/stickysession"
"k8s.io/ingress/core/pkg/ingress/errors"
"k8s.io/ingress/core/pkg/ingress/resolver"
)
Expand Down Expand Up @@ -63,8 +63,8 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
"RateLimit": ratelimit.NewParser(),
"Redirect": rewrite.NewParser(cfg),
"SecureUpstream": secureupstream.NewParser(),
"SessionAffinity": sessionaffinity.NewParser(),
"SSLPassthrough": sslpassthrough.NewParser(),
"StickySession": stickysession.NewParser(),
},
}
}
Expand Down Expand Up @@ -98,10 +98,10 @@ func (e *annotationExtractor) Extract(ing *extensions.Ingress) map[string]interf
}

const (
secureUpstream = "SecureUpstream"
healthCheck = "HealthCheck"
sslPassthrough = "SSLPassthrough"
stickySession = "StickySession"
secureUpstream = "SecureUpstream"
healthCheck = "HealthCheck"
sslPassthrough = "SSLPassthrough"
sessionAffinity = "SessionAffinity"
)

func (e *annotationExtractor) SecureUpstream(ing *extensions.Ingress) bool {
Expand All @@ -119,7 +119,7 @@ func (e *annotationExtractor) SSLPassthrough(ing *extensions.Ingress) bool {
return val.(bool)
}

func (e *annotationExtractor) StickySession(ing *extensions.Ingress) *stickysession.StickyConfig {
val, _ := e.annotations[stickySession].Parse(ing)
return val.(*stickysession.StickyConfig)
func (e *annotationExtractor) SessionAffinity(ing *extensions.Ingress) *sessionaffinity.AffinityConfig {
val, _ := e.annotations[sessionAffinity].Parse(ing)
return val.(*sessionaffinity.AffinityConfig)
}
Loading

0 comments on commit a158e5f

Please sign in to comment.