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

Cache config to improve latencies #348

Merged
merged 3 commits into from
Jan 26, 2020
Merged

Conversation

ecktom
Copy link
Contributor

@ecktom ecktom commented Jan 24, 2020

Related issue

Fixes #346

Blocked by ory/viper#7

Proposed changes

Did a first draft here. Also had to hash the entire ENV in order to allow overrides which made things like 30-40% slower. ;) Any idea for that?

Guess we could tweak things even more eg.

  • pre-allocate + copy slices instead of append
  • simple bit shift instead of binary.LittleEndian

which however would make readability worse.

Checklist

  • I have read the contributing guidelines
  • I have read the security policy
  • I confirm that this pull request does not address a security vulnerability. If this pull request addresses a security
    vulnerability, I confirm that I got green light (please contact [email protected]) from the maintainers to push the changes.
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation within the code base (if appropriate)
  • I have documented my changes in the developer guide (if appropriate)

@ecktom ecktom requested a review from aeneasr January 24, 2020 16:00
Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Looks pretty good! have some ideas to improve performance further - I've also released ory/viper with your changes!

b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, uint64(ts))

env, err := json.Marshal(os.Environ())
Copy link
Member

Choose a reason for hiding this comment

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

Because env vars can not change for a running process, I think this can be left out!

func (v *ViperProvider) hashPipelineConfig(prefix, id string, override json.RawMessage) (uint64, error) {
ts := viper.ConfigChangeAt().UnixNano()
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, uint64(ts))
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

hashSlices = append(hashSlices, s...)
}

return crc64.Checksum(hashSlices, crc64.MakeTable(crc64.ECMA)), nil
Copy link
Member

Choose a reason for hiding this comment

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

Nice pick (ECMA)!

return errors.WithStack(err)
}

c, ok := v.configCachge[hash]
Copy link
Member

Choose a reason for hiding this comment

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

We definitely need a mutex lock or we'll see panics due to concurrent access of this map!

@@ -252,6 +305,8 @@ func (v *ViperProvider) PipelineConfig(prefix, id string, override json.RawMessa
return errors.WithStack(result.Errors())
}

v.configCachge[hash] = config
Copy link
Member

Choose a reason for hiding this comment

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

I think we should make writes expensive and reads cheap and encode the config value here to JSON and have the map as json.RawMessage. This will save one json.Encode on each read!

c, ok := v.configCachge[hash]
if ok {
if dest != nil {
marshalled, err := json.Marshal(c)
Copy link
Member

Choose a reason for hiding this comment

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

Let's safe one round trip of encoding here - see line https://github.com/ory/oathkeeper/pull/348/files#diff-2449a6ea083767b149400840c05f41bdR308

I thought it might be a good idea to use glob, so I wrote a small benchmark. Unfortunately, glob seems to underperform when having to deal with interfaces (so let's stick to json), here are the results:

go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/ory/oathkeeper/docs
BenchmarkGobJSON/gob-12                     7848            151588 ns/op           55343 B/op       1234 allocs/op
BenchmarkGobJSON/json-12                   20253             57361 ns/op           32480 B/op        489 allocs/op
package docs

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"io/ioutil"
	"testing"

	"github.com/ghodss/yaml"
	"github.com/stretchr/testify/require"
)

func BenchmarkGobJSON(b *testing.B) {
	gob.Register(map[string]interface{}{})
	gob.Register([]interface{}{})

	c, err := ioutil.ReadFile(".oathkeeper.yaml")
	require.NoError(b, err)

	conf, err := yaml.YAMLToJSON(c)
	require.NoError(b, err)

	var o map[string]interface{}
	require.NoError(b, json.NewDecoder(bytes.NewBuffer(conf)).Decode(&o))

	var gb bytes.Buffer
	require.NoError(b, gob.NewEncoder(&gb).Encode(&o))

	b.Run("gob", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			m := make(map[string]interface{})
			buf := bytes.NewBuffer(gb.Bytes())
			b.StartTimer()

			require.NoError(b, gob.NewDecoder(buf).Decode(&m))
		}
	})

	b.Run("json", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			b.StopTimer()
			m := make(map[string]interface{})
			buf := bytes.NewBuffer(conf)
			b.StartTimer()

			require.NoError(b, json.NewDecoder(buf).Decode(&m))
		}
	})
}

@@ -305,7 +299,13 @@ func (v *ViperProvider) PipelineConfig(prefix, id string, override json.RawMessa
return errors.WithStack(result.Errors())
}

v.configCachge[hash] = config
marshalled, err = json.Marshal(config)
Copy link
Member

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.

yeah :D quiet obviously... Removed the unnecessary marshall...

require.NoError(t, os.Setenv("AUTHENTICATORS_OAUTH2_INTROSPECTION_CONFIG_INTROSPECTION_URL", ""))

require.NoError(t, p.PipelineConfig("authenticators", "oauth2_introspection", nil, &res))
Copy link
Member

Choose a reason for hiding this comment

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

Why were these removed?

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 was basically the root cause of my intention to cache the environment ;) But as you've correctly pointed out there will be no changes to the env for the running process... So I've simply adjusted the test to reflect this.

Copy link
Member

Choose a reason for hiding this comment

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

Ohh, I see - that totally makes sense :)

594 20119202 ns/op

v0.35.2
3048037 3908 ns/op
Copy link
Member

Choose a reason for hiding this comment

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

Wow, that perf improvement!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed :D

@aeneasr
Copy link
Member

aeneasr commented Jan 26, 2020

Awesome, thank you so much for your hard work!

@aeneasr aeneasr merged commit 95673ed into ory:master Jan 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

High latencies
2 participants