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