[HN Gopher] Build an Elixir Redis Server that's faster than HTTP
___________________________________________________________________
Build an Elixir Redis Server that's faster than HTTP
Author : weatherlight
Score : 112 points
Date : 2021-11-12 14:46 UTC (8 hours ago)
(HTM) web link (docs.statetrace.com)
(TXT) w3m dump (docs.statetrace.com)
| VWWHFSfQ wrote:
| It seems like kind of a fun hack but I'm not sure what this is
| getting you. Is this some kind of an RPC-style thing built on
| Redis? And I'm not sure where the comparison to HTTP is coming
| from. I have a lot of questions.
| secondcoming wrote:
| It's making the correct argument that handling HTTP is costly,
| and you don't always need it. People use it because it's really
| easy to get something up and running with HTTP short term, but
| long term there are better ways of doing server-server
| communication.
| hansonkd13 wrote:
| Author here. EXACTLY.
|
| I'm showing that it is just as easy to use a different
| protocol than HTTP.
|
| Instead of reaching for an HTTP framework to do RPC, I'm
| showing that Redis can be just as easy.
| handrous wrote:
| You're doing God's work. Anything reminding people that
| there's more to the Internet than the Web is a step in the
| right direction.
| VWWHFSfQ wrote:
| > People use it because it's really easy to get something up
| and running with HTTP short term
|
| And because there are decades of best-practices and standard
| ways to do very common things like authentication, caching,
| signaling errors, load-balancing, logging. Maybe a homemade
| RPC doesn't need any of that, but I would bet that at some
| point it will need at least some of it. And now you have to
| implement it all yourself.
|
| This is cool though and kinda fun to think about!
| e12e wrote:
| > decades of best-practices and standard ways to do (...)
| caching
|
| Essentially either you want RPC, or you might want caching.
| If you want to put a varnish cache in there, probably stick
| with http. If you know you're doing (very high thro / low
| latency) RPC - this might make a lot of sense.
| VWWHFSfQ wrote:
| What if I want to memoize my RPC function though? I would
| have to implement that myself I think
| travisgriggs wrote:
| Isn't the other reason that HTTP APIs are the defacto, is
| because you often don't get to control the other side of the
| equation, so we default to the "common lowest denominator"
| case?
| alberth wrote:
| https://stressgrid.com/blog/webserver_benchmark/
|
| These benchmarks, while dated and haven't been tested with the
| newly released updates to address these dynamics - might interest
| some folks.
| [deleted]
| nathell wrote:
| Tangentially related: I wish more protocols used netstrings [0]
| as transport. This would trivially eliminate a whole class of
| bugs that are all too easy to commit, especially in low-level
| languages.
|
| [0]: https://cr.yp.to/proto/netstrings.txt
| tayo42 wrote:
| Something seems wrong here. Over 1second on average to handle 1k
| http requests? I'm not sure the conclusion is valid. The test
| seems very wrong. Http being slow seems like a wild thing to say.
| Apache can serve that much
| anonymousDan wrote:
| As I understand it its just a way of building a generic service
| using the Redis protocols instead of HTTP. For example if you
| have a microservice architecture where services currently
| communicate over HTTP, you could potentially replace it with the
| Redis protocol. What I don't understand is how it compares to
| other rpc protocol formats (e.g. protocolbuffers).
| hansonkd13 wrote:
| Author here, combining Redis protocol and MsgPack is a much
| more platform independent way of building a protocol.
|
| The redis protocol and Msppack are both only a a hundred lines
| or so for a parser. Meaning you can build your own from scratch
| in a new language if one isn't supported.
|
| Its also stupid fast.
|
| Compared to protocol buffers which can be extremely complicated
| to grok on the binary level.
|
| I built a more robust API RPC for Python here based on Redis
| and MsgPack: https://github.com/hansonkd/tino
| wallyqs wrote:
| NATS has a similar protocol to Redis and optimized for this
| use cause of doing pub/sub based low latency Request/Response
| instead of HTTP. The payload is opaque so you can use msgpack
| if needed, and the protocol also supports sending headers
| now: https://docs.nats.io/nats-protocol/nats-
| protocol#protocol-me...
|
| (disclaimer: I'm one of the NATS maintainers :) )
| __turbobrew__ wrote:
| Did you consider ASN.1 for the protocol?
| e12e wrote:
| Lol. No offense, but I did a report in uni comparing a few
| alternatives (asn.1, json/xml over http, protobuf v1 (it's
| a while ago).
|
| And on one hand, sure asn.1 exists, on the other there's
| been some issues in for example nfs. I don't think you want
| asn.1 today - but maybe cap'n'proto.
|
| But i think redis is probably an interesting approach.
|
| Not sure how many of these beyond whatever/http2 have a
| sane pipelining+authenticated encryption story? I suppose
| you could "dictate" security at the ip level via
| vpn/wireguard or something. Or use Unix sockets.
| jammycrisp wrote:
| No use in changing things if it's working for you, but you
| might be interested in trying out msgspec
| (https://jcristharif.com/msgspec/) in tino instead of using
| msgpack-python. The msgpack encoder/decoder is faster, and it
| comes with structured type validation (like pydantic) for no
| extra overhead
| (https://jcristharif.com/msgspec/benchmarks.html).
| jeffbee wrote:
| > extremely complicated to grok on the binary level.
|
| That seems like a weird claim. Protobuf on the wire only has
| four types: short fixed-length values, long fixed-length
| values, variable-length values with continuation-bit
| encoding, and length-prefixed values, where the length is
| continuation-bit encoded.
|
| Msgpack has 37 different wire types!
| latch wrote:
| In Elixir / Erlang, you can put a socket in either active or
| passive mode. In passive mode, you need to explicitly call recv/2
| or recv/3 to get data, very similar to a traditional socket API.
| This is what this code appears to be doing.
|
| But if you want better performance, you use active mode. In
| active mode, the runtime is receiving data on your behalf, as
| fast as possible, and sending it to the socket's owning process
| (think a goroutine) just as fast. Data is often waiting there for
| you, not just already in user space, but in your Elixir process's
| mailbox. (Also, this doesn't block your process the way recv/2
| does, so you could handle other messages to this process.)
|
| You could imagine doing something similar with 2 goroutines and a
| channel. Where 1 goroutine is constantly recv from the socket and
| writing to a buffered channel, and the other is processing the
| data.
|
| One problem with active mode, and to some degree how messages
| work in Elixir in general, is that there's no back pressure.
| Messages can accumulate much faster than you're able to process
| them. So, instead of active mode, you can use "once" mode or
| {active, N} mode (once being like {active, 1}. In these modes you
| get N messages sent to you, at which point it turns into passive
| mode so you can recv manually. You can put the socket into
| active/once mode at any point in the future.
| spockz wrote:
| You could implement backpressure by either adding a buffer
| (which you already have here) or by rejecting requests
| optionally with more data on how hard and long to back off.
| jadbox wrote:
| If you can tolerate (debounced) data loss in the buffer, a
| ring buffer works really well with predictable memory and
| performance.
| new_stranger wrote:
| Since you relate this example to Go, would you mind sharing
| thoughts about how heavy I/O from network sockets compares
| between the two or gotchas that might not be apparent to
| Erlang/Go developers about the other?
| dnautics wrote:
| is there somewhere where there is a comparison of the
| performance between active and passive mode? I don't imagine it
| to be significant for most use cases, so the extra safety
| seemed worth it to me in all the libraries I wrote, though I
| suppose it wouldn't be hard to rewrite those with {active, 1}.
| devoutsalsa wrote:
| Using active mode is a nice because you can receive other
| messages as well, instead of blocking to receive just a tcp
| message. I like active once myself.
| jeffbee wrote:
| Seems like the author should have stopped when they concluded
| that it takes 2 seconds to serve an HTTP request before
| publishing this and embarrassing themselves. gRPC C++ serves
| HTTP/2 in less than 120 _micro_ seconds, which includes server-
| to-server network flight time.
|
| https://performance-dot-grpc-testing.appspot.com/explore?das...
| dljsjr wrote:
| If the idea is to leverage the Redis protocol for an "API", what
| are the benefits to this approach over using the built-in `pub-
| sub` and the regular old C implementation of Redis Server?
|
| I could maybe see some really specific use-cases for this but for
| probably 90% of cases implementing a distributed process API
| should be able to go over pubsub fine shouldn't it? Or does
| pubsub have some sort of massive overhead?
|
| EDIT: Just because it's tangential to the topic at hand, and
| since I'm up here at the top, I also wanna throw out a nod to
| libraries like `nng` and `nanomsg` which are spiritual successors
| to ZeroMQ and they have functionality like brokerless RPC/pubsub
| built in as messaging models. I don't see tools like this talked
| about a lot in this space cuz systems software isn't the sexiest
| but if you need to embed a small and lightweight messaging
| endpoint in your backend stuff then look at those as well. No
| horse in the race, just like sharing useful tools with people.
|
| https://nng.nanomsg.org
| hansonkd13 wrote:
| This article is about implementing the Redis Protocol on a
| socket yourself. Not about the Redis Database. Sorry for the
| confusion.
| dljsjr wrote:
| I actually did understand that and I think it's super cool
| from a technical perspective. I was just curious as to the
| use case where this was a strong contender to beat out the
| built-in stuff you can get from Redis server. It's a "small"
| implementation but you still have to test it and own it
| throughtout the lifecycle of the product that's using it.
| ellyagg wrote:
| It's pretty common that you won't have a specific use case
| in mind when learning something new, but I've often found
| that later it ends up fitting in somehow.
| mst wrote:
| If it's straight RPC there's no need for the extra moving
| parts, and the simpler your topology the fewer ways for it to
| go wrong.
|
| I often build TPC based "protocols" that are just newline
| delimited json in each direction, it's a nice middle ground
| between using an HTTP POST and something like gRPC.
| dljsjr wrote:
| I'm not 100% sure I'm understanding but if the idea is to
| implement a "traditional"/synchronous RPC mechanism you can
| do that using the PubSub functionality trivially and I'm not
| sure which additional moving parts it adds unless you're in a
| situation where you have to cluster Redis (which has a
| reputation for being a PITA but isn't _that_ hard honestly).
|
| The only use case that _really_ jumps out is when you don't
| want a broker because you don't want a single point of
| failure but now you're embedding a Redis server
| implementation in every one of the services in your mesh and
| I'm not convinced that's much better but I can see where it
| might be helpful.
| mst wrote:
| > I'm not sure which additional moving parts it adds
|
| > The only use case that really jumps out is when you don't
| want a broker
|
| The broker is exactly the additional moving part I was
| referring to.
|
| Pubsub over a broker is more complicated than RPC
| operationally, whether you're already using the broker
| software elsewhere or not.
|
| Especially when you're looking at the RPC server living
| inside an erlang VM that's already really good at handling
| things like load shedding of direct connections.
| anonymousDan wrote:
| I don't think this is anything to do with Redis server, you
| are simply using the Redis wire protocol/parser? I could be
| wrong...
| hansonkd13 wrote:
| Correct this post is about the Redis Protocol.
| dljsjr wrote:
| That's correct but my point was that Redis has an out-of-
| the-box solution for "message passing arbitrary data
| using the Redis protocol over TCP to named endpoints",
| and Redis Server is a very lightweight piece of software.
| Even if you aren't using it as an in-memory key-value DB
| it's not a big problem to pull it in to your stack even
| if you're only gonna use it for PubSub or RPC/IPC.
|
| This is a super cool write-up and I'm not saying anything
| negative about what the author did. I like it a lot. I'm
| just asking from a technical and curiosity perspective
| what the advantages are to this over using the stuff
| Redis Server already provides which can do the same
| thing.
| mrkurt wrote:
| With Elixir specifically, you have the option of
| clustering these things. We (fly.io) send messages
| between servers using NATS. This works well with
| geographically distributed infrastructure, messages are
| somewhat peer to peer. If we were using Redis, we'd need
| a round trip to a centralized server. And we'd need the
| internet to always work well.
|
| You can do a similar thing with Elixir and your own
| protocol (or the Redis protocol).
| hansonkd13 wrote:
| I suppose the main advantages would be less dependencies
| (no Redis server). Less overhead (you aren't routing
| though anything)
|
| I haven't looked at the exact performance characteristics
| but it would be fun! You would have built in load
| balancing!
| derefr wrote:
| > and the regular old C implementation of Redis Server?
|
| To be clear, this article is talking about implementing your
| own synchronous-RPC-request server, i.e. a network service that
| other services talk to through an API over some network wire
| protocol, to make requests over a socket and then wait for
| responses to those requests over that same socket. This article
| _assumes that you already know that that 's what you need_.
| This article then offers an additional alternative to the
| traditional wire protocols one might expose to clients in a
| synchronous-RPC-request server (RESTful HTTP, gRPC, JSON-RPC
| over HTTP, JSON-RPC over TCP, etc.); namely, mimicking the wire
| protocol Redis uses, but exposing your own custom Redis
| commands. This choice allows you to use existing Redis client
| libraries as your RPC clients, just as writing a RESTful HTTP
| server allows you to use existing HTTP client libraries as your
| RPC clients.
|
| The alternative to doing so, if you want to call it that, would
| be to write these custom commands as a Redis module in C. But
| then you have to structure your code to live inside a Redis
| server, when that might not be at-all what you want, especially
| if your code already lives inside some _other_ kind of
| framework, or is written in a managed-runtime language unsuited
| to plugging into a C server.
|
| Or think of it like this: this article is about taking an
| existing daemon, written in some arbitrary language (in this
| case Elixir), where that daemon already speaks some other,
| _slower_ RPC protocol (e.g. REST over HTTP); and adding an
| additional Redis-protocol RPC listener to that daemon, so that
| you can use a Redis client as a drop-in replacement for an HTTP
| client for doing RPC against the daemon, thus (presumably)
| lowering per-request protocol overhead for clients that need to
| pump through _a lot_ of RPC requests.
|
| I do realize that you're suggesting that you could use Redis as
| an _event-bus_ between two processes that each connect to it
| via the Redis protocol; and then use a "fire an async request
| as an event over the bus, and then await a response event
| containing the request's ref to show up on the bus" RPC
| strategy, ala Erlang's own gen_server:call messaging strategy.
| All I can say is that, due to there being _three_ processes and
| _two_ separate RPC sessions involved, with their own wire-
| protocol encoding /decoding phases, that's likely higher-
| overhead than even a direct RESTful-HTTP RPC session between
| the client and the relevant daemon; let alone a direct Redis
| RPC session between the client and the daemon.
| dljsjr wrote:
| That's fair. Although I think the characterization of doing
| RPC over Redis's built-in pubsub is a little uncharitable.
| You'd just fire off a PUBLISH command w/ the channel and
| payload, and you'd receive the response to the RPC request on
| a subscriber that you can immediately drop (a pseudo-one-shot
| channel). It doesn't have to be an event bus (even though
| that's close to how Redis does pubsub internally).
| derefr wrote:
| When I say "event bus", I mean "an async RPC architecture
| using a reliable at-least-once message-queuing model, where
| clients connect to a message broker [e.g. a Redis stream],
| and publish RPC workloads there; backends connect to the
| same message broker, and subscribe as consumers of a shared
| consumer-group for RPC workloads, greedily take messages
| from the queue, and do work on them; backends that complete
| RPC workloads publish the workload-results back to the
| broker on channels specific to the original clients, while
| ACKing the original workloads on the workloads channel; and
| clients subscribe to their own RPC workload-results
| channel, ACKing messages as they receive them."
|
| _Event bus_ is the name for this network architecture. And
| if you 're trying to replicate _what synchronous client-
| server RPC does_ in a distributed M:N system, it 's what
| you'd have to use. You can't use at-most-once/unreliable
| PUBSUB to replicate how synchronous client-server RPC
| works, as a client might sit around forever waiting for a
| response that got silently dropped due to the broker or a
| backend crashing, without knowing it. All the queues and
| ACKs are there to replicate what clients get for free from
| having a direct TCP connection to the server.
|
| (Yes, Erlang uses timeouts on gen_server:call to build up
| distributed-systems abstractions on top of an unreliable
| message carrier. But everything else in an Erlang system
| has to be explicitly engineered around having timeouts on
| one end and idempotent handling of potentially-spurious
| "leftover" requests on the other. Clients that were
| originally doing synchronous RPC, where you don't know
| exactly _how_ they were relying on that synchronous RPC,
| _can_ switch to a Redis-streams event-bus based messaging
| protocol as a drop-in replacement for their synchronous
| client-server RPC, because reliable at-least-once async
| delivery can embed the semantics of synchronous RPC; but
| they _can 't_ switch to unreliable async pubsub as a drop-
| in replacement for their synchronous client-server RPC.
| Doing the latter would require investigation and
| potentially re-engineering, on both sides. If you don't
| control one end -- e.g. if the clients are third-party
| mobile apps -- then that re-engineering might even be
| impossible.)
| dlsa wrote:
| > This article assumes that you already know that that's what
| you need
|
| This is how I read the article. It was about how to implement
| network protocols in elixir and here are two of them: redis
| and msgpack.
|
| Having an elixir based redis server is not the same piece of
| the puzzle as having elixir simply talk to a redis server.
| For one, the elixir based redis server can have arbitrary
| rules around keys and values that are not supported by redis.
| (Same said for a redis server written in C or python or rust
| or...)
|
| This approach lets you store all keys and values in a dict,
| b-tree, sqlite or postgres etc. Want to store each value in a
| flat file? Sure, now you can. Only you know if this is
| actually useful.
|
| At least, this is how I made sense of this.
___________________________________________________________________
(page generated 2021-11-12 23:01 UTC)