[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)