-
Notifications
You must be signed in to change notification settings - Fork 3
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
Vtgateproxy rework discovery and add metrics #296
Conversation
This way we only have a single entity watching the file and dispatching out to the resolvers when things change rather than a bunch of tickers watching the file. Also cleaned up the code a bunch.
"vitess.io/vitess/go/exit" | ||
"vitess.io/vitess/go/stats/prometheusbackend" | ||
"vitess.io/vitess/go/vt/servenv" | ||
"vitess.io/vitess/go/vt/vtgateproxy" | ||
) | ||
|
||
func init() { | ||
rand.Seed(time.Now().UnixNano()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure this is not needed since we never use the global rand.
@@ -31,14 +30,8 @@ import ( | |||
"vitess.io/vitess/go/vt/log" | |||
) | |||
|
|||
var ( | |||
jsonDiscoveryConfig = flag.String("json_config", "", "json file describing the host list to use fot vitess://vtgate resolution") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seemed clearer to put all the flag definitions in the same place in the main vtgateproxy file.
continue | ||
} | ||
|
||
// notify all the resolvers that the targets changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the core of the change - the resolver "builder" is responsible for watching the file.
Each target then gets a separate resolver class which we track in a list, and notify whenever something changes in the file, at which point the resolver turns around and picks a new set of hosts out of the global list.
} | ||
hosts = candidates | ||
|
||
// Handle both int and string values for port |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is somewhat regrettable but for silly consul template reasons, the grpc port is a string in our file format.
In talking to @jscheinblum about this, an upstream implementation could be simplified if we wanted to enforce a file format with hard-coded field names, which is more consistent with the rest of the package and then we wouldn't need all these xyzField command line flags.
It would of course mean that we would need to render a second file for vtgate hosts in the format that the proxy wants, or change webapp to use this other format.
Regardless, this approach seemed like the cleaner one for us for now.
// the affinity matching (e.g. same az) hosts at the front and the non-matching ones | ||
// at the end. | ||
// | ||
// Only need to do n-1 swaps since the last host is always in the right place. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some testing of this on the playground:
https://go.dev/play/p/a_nnDVEgkq-
func (r *JSONGateConfigResolver) Close() { | ||
r.ticker.Stop() | ||
|
||
// Utilities |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alas this seems silly but it's apparently the "canonical" golang way to do it.
affinityAttr = flag.String("affinity_attr", "", "Attribute (both mysql protocol connection and JSON file) used to specify the routing affinity , e.g. 'az_id'") | ||
vtgateHostsFile = flag.String("vtgate_hosts_file", "", "json file describing the host list to use for vtgate:// resolution") | ||
numConnections = flag.Int("num_connections", 4, "number of outbound GPRC connections to maintain") | ||
poolTypeField = flag.String("pool_type_field", "", "Field name used to specify the target vtgate type and filter the hosts") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another change to the flags so we'll need to update the dockerfile again.
go/vt/vtgateproxy/discovery.go
Outdated
|
||
func (r *JSONGateConfigResolver) resolve() (*[]resolver.Address, []byte, error) { | ||
// Start a config watcher | ||
b.ticker = time.NewTicker(100 * time.Millisecond) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ten times a second is probably overkill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. It's only a stat
call however. Also this isn't changed from the current implementation (I don't think). But yeah maybe 1 / sec is fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is, if there's an error, it's going to log ten times a second.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smart. 1 / sec sound good to you? Alternatively we could stat faster and damp the error logs?
go/vt/vtgateproxy/discovery.go
Outdated
log.Errorf("Error stat'ing config %v\n", err) | ||
continue | ||
} | ||
isUnchanged := checkFileStat.Size() == fileStat.Size() || checkFileStat.ModTime() == fileStat.ModTime() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be &&
? file sizes can stay constant, particularly if only hostnames are changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also just moved around, but you're definitely right. Will change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I did this change and then tested in dev:
Touch the file (unchanged, no reparse)
Add whitespace to file (reparsed)
Single character change to file (reparsed)
affinity := "" | ||
if b.affinityField != "" { | ||
affinity = attrs.Get(b.affinityField) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I have this right: grpc will call Build
when a new client connection is created, which is identified by the vtgate://<type>
portion of the URL.
So if we ask for the same <type>
but with two different affinity attributes, we will only create one resolver corresponding to the first affinity.
This likely works in practice because why would we change the affinity from the local AZ? But it seems more correct to defer affinity to the actual subconn picking step (which would probably mean implementing a custom load balancer).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it seems more correct to defer affinity to the actual subconn picking step (which would probably mean implementing a custom load balancer).
That's how @jscheinblum originally implemented it before the refactor that preceded this refactor.
The problem is that GRPC will connect to every host that you return from the discovery list. So you end up with all these actually unused TCP connections. So, we had to move the affinity check and the subsetting into the discovery phase and not the load balancing phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I have this right: grpc will call Build when a new client connection is created, which is identified by the vtgate:// portion of the URL.
Not quite -- Build will be called based on the full URL uniqueness. So even if you change things so there's a different az_id or we omit az_id altogether, it will get a new set of hosts for that target.
I just verified this in dev and tweaked the metrics a bit so that if we ever don't know the az_id of the target host we now track it as "unknown"
|
||
hash = newHash | ||
b.update(r) | ||
b.resolvers = append(b.resolvers, r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we never clear this is because we expect to create exactly one resolver per target during the lifetime of the process, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. We either reuse the existing resolver or create a new one, we never need to remove them.
* refactor a bit more to consolidate the command line flags * rework again to have a single parser and move around all the logic This way we only have a single entity watching the file and dispatching out to the resolvers when things change rather than a bunch of tickers watching the file. Also cleaned up the code a bunch. * redo the shuffle to be a single pass * actually use the builder rand not the global one * add some metrics * only stat once per second * split out counter for unknown affinity
* refactor a bit more to consolidate the command line flags * rework again to have a single parser and move around all the logic This way we only have a single entity watching the file and dispatching out to the resolvers when things change rather than a bunch of tickers watching the file. Also cleaned up the code a bunch. * redo the shuffle to be a single pass * actually use the builder rand not the global one * add some metrics * only stat once per second * split out counter for unknown affinity
Description
Refactor the discovery logic to have a single watcher for the parsed file, and then each resolver just gets callbacks from there as things change.
Clean up and consolidate the command line flags, and change a bit. The new set of options to run (tested in dev) are:
Implement a more efficient single pass shuffle scan because it seemed nifty.
Add some basic metrics for the discovery part: