HAproxy in the era of Microservices
47ron.in
47ron.in
The thing which has made our lives MUCH easier from an orchestration, discovery, and routing perspective is NSQ, an OSS message bus. It's an event model rather than a request model -- producers of events say "Hey world, by the way, EventType just happened." and consumers can register to say "Apropos of nothing, please tell me about any EventType" without producers or consumers having to be aware of each other or aware of what a consumer intends to actually do with regards to the event.
Events go into topics ("a type of event") and each topic can have an arbitrary number of channels ("one thing which should happen for each event in this topic"). If you're clever about where you raise events, you can stitch together arbitrarily complex systems by just adding new consumers on new channels, without having to make the producers aware of the new functionality at all.
Concrete example: We wanted a log of all orders made to the stock exchange. We have a little utility, nsq-to-s3, which takes all the events on a topic/channel, persists them in memory/disk for a while, and periodically flushes that log to S3 for long-term storage. This trivialized the logging feature. (I was going to write our own nsq-to-s3 but an OSS project of that same name existed and was adequate to our needs, so we didn't have to go to 8 microservices... yet.)
This was much easier than making a generic S3 logging service and then making edits to seven microservice codebases to make a HTTP request out to the logging service, with e.g. retry/failure recovery/monitoring/etc, everywhere in those codebases we wanted something logged to S3.
It really starts to shine when you add controls to the consumers to enable soak testing of new services by either redirecting some small % of traffic to the new version dynamically.
Did haproxy not support the necessary ACLs for you to accomplish this? It might be possible in 1.6, which lets you process the HTTP request body in addition to the headers.
http://blog.haproxy.com/2015/10/14/whats-new-in-haproxy-1-6/
Basically I'd like the incoming router to send a message of a specific type and subtype to an existing listener, but if one does not exist I'd like to take back the message, spawn a new listener which will get on the queue and resend the message.
I wonder if I could use a messaging service for keeping track of this, rather than have to manage my own map of what application backends are ready to process which type of messages ( spawning that backend takes a lot of resources so I want them to be born and die based on demand).
Along those same lines how do you deal with fetching of domain objects and associated relationships from these services? For example, let's say in one instance you want a user and their manager and in another instance you want a user and their manager's manager and so forth for any relationship that a user might have (that the User service is responsible for).
It looks like there is a lot of good advice on this thread for how to mitigate the configuration problem (bamboo, consul template, etc ...) but then you run into reloads, and even if you use zero downtime restarts as I described in my HAProxy deep dive (http://engineeringblog.yelp.com/2015/04/true-zero-downtime-h...), you still end up with periodic ~50ms blips in your timings.
We've found that Synapse (https://github.com/airbnb/synapse) is best in class at managing HAProxy for us in a fairly large SOA. It is really intelligent about doing as much as possible dynamically over the HAProxy stats socket as opposed to reloading for every configuration change, and we can finely tune how often we are reloading HAProxy. The best part is that it is fully pluggable. Use DNS, marathon, zookeeper, etcd, etc...: Synapse can manifest that service registration information into HAProxy configurations.
The good part about HAProxy is that it's flexible and can be configured any number of ways, but I've found that flexibility can weigh some teams by using it to solve problems that it's really not designed for. Sometimes it's better to use a brute-force approach with a simple configuration and deal with the complexity in a tool better designed for complexity like Mesos.
See also http://prometheus.io/ and https://www.youtube.com/watch?v=HjN23GgCzQY for an intro.
Most companies don't need microservices, so I'm making an assumption that any company using microservices has both the volume and complexity of business partners that make a microservices approach a good business idea (this assumption is assuredly false, but still useful). And if you've gone this route, the next step is generally organizing your business around an SOA where business units can be represented by an API (microservices are useful from a business perspective because you have many stakeholders within a company that want to make small changes frequently but independently). Once you've done that, you start wanting to encapsulate your relationship with your customers in an API as well, and the eventual destination (if your company survives long enough and is successful) is that you just operate a set of APIs that power your customer-facing UX product in addition to the API-based services you provide to your high-volume clients.
My point is this: the end state of a microservices architecture is an API gateway. API gateways provide essential services such as authentication, access control, and connection throttling. You still need something like HAProxy to provide you with load balancing, health checks, etc. across many nodes - but you're going to have a very simple instance of HAproxy for each microservice and rely on your API gateway to organize everything under a single domain.
With microservices, your configurations can get incredibly complex very quickly, so it's often better to maintain independent components with very simple configurations (that can be easily updated dynamically) than to try to have a single component in the middle of your stack upon which many other components are dependent.
resolvers docker
nameserver dnsmasq 127.0.0.1:53
defaults
mode http
log global
option httplog
frontend f_myapp
bind :80
default_backend b_myapp
backend b_myapp
server s1 nginx1:80 check resolvers docker resolve-prefer ipv4
[1] http://blog.haproxy.com/2015/10/14/whats-new-in-haproxy-1-6/Thanks for the heads up. Missed that one!
Full disclosure: I am the lead developer of Baker Street :)
Website: http://bakerstreet.io
Just a small nitpick: Running Zookeeper is insanely simple. We deployed it in 2-3 days, and it has had literally 0 issues over the last year.
I highly recommend https://github.com/mbabineau/docker-zk-exhibitor to get started (Docker container that wraps together Zookeepher and Netflix's Exhibitor manager for it).
Remember, pick boring technologies that just work.
http://blog.cloudera.com/blog/2014/03/zookeeper-resilience-a...
http://arstechnica.com/information-technology/2015/05/the-di...
https://tech.knewton.com/blog/2014/12/eureka-shouldnt-use-zo...
Here's ibm and netflix with some advice about how non-boring running a ZK cluster can be:
https://www-01.ibm.com/support/knowledgecenter/SSCRJU_4.0.0/...
http://techblog.netflix.com/2011/11/introducing-curator-netf...
We have had very abstract conversations about adding alternative back ends, but presently we are not working on support for that feature because as you correctly noted, at least for Consul and ZooKeeper it is somewhat against the operating philosophy.
We're not opposed to it though and if you open a feature request we'll track interest which will help us decide direction. Baker Street is not a secondary component for some other system in our company. We consider Baker Street a core product even at this early stage in its development.
> because we wanted a simple, highly available service, not a strongly consistent one.
I absolutely agree with this design choice.
This is the big flaw for me with how a lot of setups use Etcd, Consul, Zookeeper and the like.
A lot of the time they're being used because we need high-availability, not at all for their consistency guarantees, and their consistency guarantees has great potential to reduce the availability.
I've not run Zookeeper in production, but both with Etcd and Consul I've had situations where the clusters have gotten into totally broken states because the cluster membership has changed in bad ways. It makes sense for the guarantees they are providing. It does not make sense if you don't need the consistency guarantees.
The way I see it, lost service registrations are a non-issue as you generally want to continuously renew them combined with a TTL to avoid stale registrations, so the occasionally lost one due to e.g. a part of the service directory cluster failing at a bad time is no big deal. Conversely, if a service stays in the service directory when it shouldn't, the worst that happens is that you run into an unhealthy service when you try to issue requests, but that will happen during normal operation while using services with consistency guarantees anyway, since the service may have failed between health checks anyway, which means you still need to handle retries.
I'd rather use a service-discovery solution with fairly significant consistency issues that keeps replying "no matter what" than one which focuses on consistency but may stop answering.
We route based on `sub`, `prefix`, `appname`, and even sometimes `path`. Managing the config is literally going into this relatively big file, adding in ACLs, adding backends, and setting up the routing rules. We manage the file with puppet, but it's static - not generated.
I feel that it's probably time to start deploying many HAProxy instances, generating the file per service, and then maintaining a frontend haproxy that routes according to subdomain.
Can anyone comment on similar deployments? Have you found it easier to manage many haproxy instances rather than a single monolithic one?
Having been a long-time user, I can tell you the added complexity of every service having to allocate its own port, then tracking that port across every other service is a lot of overhead on its own.
Not to mention you also have to run a zookeeper cluster and two additional daemons on every instance.
All in all it's a good concept, but slightly too complex for my needs. Recently made the switch back to using internal ELB's for every service and not looking back.
That said, would prefer not to be using nginx, so if anyone has any recommendations for high quality zero-downtime-reloadable HTTP proxies, I'd be very interested.
Looks like bamboo encourages folks to use this strategy via a custom reload command: https://github.com/QubitProducts/bamboo/issues/152
That being said, you can turn ingress into egress with an ifb. A coworker of mine created a proof of concept for using our strategy with an external facing load balancer but I don't think he ever tried it in production so I'm not sure how well it works.
To be fair I'm scared of both the iptables and tc solution on external LBs because you might never know that you're refusing connections accidentally. Linux 4.4 is coming with some patches that should help with this and afaik BSDs have done it right since the start.
Disclosure: I'm the author
Yes, though a bit of advanced planning and creative configuration can usually work around this limitation.
Making the assumption that new servers and services occur within a predictable range of IPs and ports (or have their own haproxy to route within the server), you can create a list of backend servers which are all disabled and just waiting to be individually enabled.
I've done this in the past, and the disabled server entries never seem to have a negative impact on HAProxy. It also has the advantage that you can set your HAProxy instances to peer with each other, so the change only has to be made once to propagate to connected instances.
Also consul only has two levels of services right, "local" and "remote"? I'm not sure that is sufficiently powerful to describe SOA's which need to be able to differentiate services on the machine, rack, colo, datacenter, region locality? I suppose you could do what we do in SmartStack and just register the same service instance at a few different places since I think that consul can register arbitrary services.
Maybe all we need is a consul watcher in Synapse, and a consul-template for the Synapse configs, and we can get the best of all worlds. With Linux 4.4 SYN handling in Linux might even be fixed by Eric Dumazet being super hardcore (e.g. https://lwn.net/Articles/659199/) and we won't even have to worry about reloading HAProxy.
Disclosure: I'm the author.
This is related to https://github.com/hashicorp/consul-template/issues/165#issu... although that is even more extreme.
Maybe I'm not understanding the situation correctly, and granted, I've not ran it on a cluster of more than 5 machines with more than 10 microservices as we decided to use Rancher instead.
Why not nginx?