Hacker News new | comments | show | ask | jobs | submit login
Fast, Safe, and Complete(ish) Web Service in Rust (brandur.org)
359 points by henridf 8 months ago | hide | past | web | favorite | 122 comments



Really nice introductory article, though some of the more… axiomatic assertions, are iffy.

> If it’s possible to build more reliable systems with programming languages with stricter constraints, what about languages with the strongest constraints? I’ve skewed all the way to the far end of the spectrum and have been building a web service in Rust

Bit overselling it, that introduction would work better for something like ATS or possibly Idris. Rust has its strictness, but it's not exactly "you must prove this recursion terminates"-strict

> Synchronous operations aren’t as fast as a purely asynchronous approach

This is misleading. Synchronous sequences are generally faster than asynchronous ones because yielding and getting scheduled back has a non-zero overhead (this is regularly re-discovered, most recently by the developers of "better-sqlite": https://news.ycombinator.com/item?id=16616374).

Async operations are a way to increase concurrency when your synchronous sequence would just be waiting with its thumb up its ass (e.g. on a network buffer filling or on a file read completing), so you can provide more service over the same wallclock time by (hopefully) always using CPU time.


> Synchronous sequences are generally faster than asynchronous ones because yielding and getting scheduled back has a non-zero overhead

Indeed. I remember being shocked when I first read "Thousands of Threads and Blocking I/O" [0] by Paul Tyma.

After reading the C10k problem [1], I just believed that a big pile of threads would never work and never updated my beliefs. While such an approach is still quite memory limited compared to asynchronous approaches, it scales quite far and can have a very high work rate.

[0] https://www.slideshare.net/e456/tyma-paulmultithreaded1

[1] http://www.kegel.com/c10k.html


i'd say in most projects i worked, threads would be enough. but it is so boring :) new shiny async code is much more entertaining!

on other hand we should talk about C100k or C1m problems.


Someone actually coined the C10M problem a while back [0]. It looks like it hasn't been updated in quite a while, though.

http://c10m.robertgraham.com/p/blog-page.html


The problem is that, in practice, many of those thread contexts are holding open resources like DB connections or file handles, and the costs related to those mount pretty quickly.

This is why actor models are good IMO. They are based on the actual resource model, rather than the developer's mental model. That is, until the developer learns to change his/her mental model.


Allow me to third that. It's one of dangers assuming that what you learned at university 15-20 years ago still applies. Another interesting talk: [0] (and even that is 5 years old)

[0] https://www.youtube.com/watch?v=KXuZi9aeGTw


In other words, for data-bound processing (such as most web services), async approaches are faster (specifically they have greater throughput).


I'm confused about your "greater throughput" comment. Generally one achieves maximal throughput by maximizing parallelism (not plain concurrency!) and letting each individual core/CPU run its job to completion while ignoring everything else (even interrupts, as far as applicable).

Async is generally associated with lower latency.

Can you expound?


You can keep pushing bytes as you get them from async operations. In my nodejs am making 20000 DB queries and I can push data to connected clients when any these get completed.

In the threaded code, you need to get a window of time and memory. In my Java, I have a max heap size of 8gb and I can maybe create up to 8k threads. When making 20000 DB queries only 8k is being executed where most of these are waiting for the query to complete or CPU time.


You're wrong about thread limit. I can easily create 100k threads on my PC and it takes around 6GB memory (and I didn't even bother to investigate that consumption, probably I could do with less). 8k threads is nothing. And beefy server machine should be able to deal with million of threads (but I guess that's around practical limit).


This depends on thread implementation and how much is allocated for the stack. Java will by default allocate 1MB, you can change that if you know what are you doing.

I know that you can have millions of M:N/green threads but these are different. In any case, you need to compete for memory and CPU time to execute.

Since not many people are programming in Haskell and BEAM languages my simplifications still hold.


I'm talking about real threads, not green threads and I used Java for my quick test. Here's code, you can run it yourself. masklinn is correct about virtual memory, every sane OS will only use few kilobytes for short stacks.

    public class Test {
        volatile static boolean stop = false;
        static AtomicInteger counter = new AtomicInteger();
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[100000];
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(() -> {
                    counter.incrementAndGet();
                    while (!stop) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException ignored) {
                        }
                    }
                });
            }
            for (Thread thread : threads) {
                thread.start();
            }
            while (counter.get() < threads.length) {
                Thread.sleep(1000);
            }
            System.out.println("Threads are running");
            Thread.sleep(10000);
            stop = true;
            for (Thread thread : threads) {
                thread.join();
            }
        }
    }


> This depends on thread implementation and how much is allocated for the stack. Java will by default allocate 1MB, you can change that if you know what are you doing.

Isn't that vmem? Unless it also writes to the entirety of that 1MB before starting the thread, it's not actually committed by default on most unices.


> This is misleading. Synchronous sequences are generally faster than asynchronous ones because yielding and getting scheduled back has a non-zero overhead

To get a bit pedantic, synchronization introduces overhead. If you've got an 8-core processor running things async/parallel may complete faster since you aren't pinned to one core.

That said, it's much harder to get right since you have to deal not only with thread sync overhead but also cache/pipeline sharing and a bunch of other gnarly things.


Even without formal synchronization in user space code, context switches have overhead.


No. Implicitly yielding and getting scheduled back is exactly what synchronous sequence is. And it cannot ever be faster than asynchronous, because asynchronous doesn't yield. Any "rediscoveries" are just mistakes due to lack of fundamental understanding of these things.


> No. Implicitly yielding and getting scheduled back is exactly what synchronous sequence is.

Your asynchronous system can be descheduled by the OS on top of the stuff it does on its own if it runs on a preemptive multitasking OS or hypervisor, which more or less all of them do.

> And it cannot ever be faster than asynchronous, because asynchronous doesn't yield.

The asynchronous system yields doubly, it yields to its own userland scheduler/event loop and it's de-scheduled by the preemptive multitasking OS it runs on top of.


Any process or thread can be descheduled by the OS on top of the stuff it does on its own.

Yielding is a thing from the multithreaded world with synchronous APIs. Asynchronous systems don't yield, they are programmed asynchronously. They have no yielding overhead and can never be slower than yielding threads.

If you are arguing for 1:1 kernel threading against M:N threading (coroutines, goroutines, green threads) that's fine, but multithreading implementations have absolutely nothing to do with asynchronous systems.


It seems like the two of you disagree fundamentally on what "asynchronous" means in this context.

https://en.wikipedia.org/wiki/Asynchrony_(computer_programmi...


I don't think we disagree on that, since he explicitly mentions event loops and essentially claims they are generally slower. Which is nonsense.


All of these examples are running on preemptive multithreaded operating systems, so implicitly yielding and getting scheduled is in the cards even if your process is managing its own concurrency with cooperative multithreading.


So where are the benefits coming from in these "rediscoveries"?


Sometimes async is not real async, but an emulation, this is where "rediscoveries" come in I suppose.


Why would you want to do that in Rust instead of Go / Java / C# since the performance is the same. When I look at code example in the article the syntax is just awful seriously, who want to write web services with that kind of syntax:

time_helpers::log_timed(&log.new(o!("step" => "upsert_episodes")), |_log| { }

It's just non-sense:

impl<S: server::State> actix_web::middleware::Middleware<S> for Middleware {

        fn start(&self, req: &mut HttpRequest<S>) -> actix_web::Result<Started> {

            let log = req.state().log().clone();
            req.extensions().insert(Extension(log));
            Ok(Started::Done)
        }
Rust is a good language but I don't see it any time soon in the web space with that syntax and the lack of mature libraries, plus the time it takes to get anything done. Where I see it is more for super optimized things like serialization and the like as libraries but not as a main program.


Nonsense to you is wonderful to me. I don't think "I am unfamiliar with this syntax" is really a useful observation - Erlang has an extremely unfamiliar syntax, but I don't think that makes it less suitable for building a service.

To answer your question more directly (though I feel the article covers these points), as someone building services in rust, there are a few reason I'd choose Rust over the languages you listed:

a) Largely deterministic performance

Having written services professionally in Java I have seen performance scale linearly with connections, until one day it suddenly spikes massively - why? It was that we crossed some threshold where the GC was suddenly having to do a ton more work, which caused a serious feedback loop - server has to pause, clients don't handle it well, clients hammer server more, server has to pause, etc.

You can have similar issues in Rust but they should be easier to spot (they should be less blow-up-y).

b) Correctness/ Expressiveness (is this a word?)

I find it far, far easier to write correct, expressive code in Rust. For example, I think anyone who has moved from Java 6 to 7 or 8 will admit that Optional is awesome - because it is. In Rust the pattern exposed by Optional is first class, and extends far beyond nullability. This is just one example though.

I find it extremely easy, by comparison to other languages, to write Rust code in a 'type driven' way, and in my experience this is a huge part of (me) writing code with fewer defects.

As the linked article puts it:

> Constraints like a compiler and a discerning type system are tools that help us to find and think about those edges.


Nonsense to you is wonderful to me. I don't think "I am unfamiliar with this syntax" is really a useful observation - Erlang has an extremely unfamiliar syntax, but I don't think that makes it less suitable for building a service.

I do not think this is just about unfamiliarity. Doing static dispatch with generics + traits does add complexity to type signatures. E.g. consider using dynamic dispatch using trait objects

  trait Frobber {
    fn frob(&self, something: &OtherTrait);
  }
versus static dispatch:

  trait Frobber {
    fn frob<S>(&self, something: &S) where S: OtherTrait;
  }
Of course, static dispatch has large benefits (opportunities for inlining, possibly leading to other optimalizations). Rust adds correctness and deterministic performance (besides Rc/Arc misuse). But Rust also has the potential for 'macro/generics soup' and over-abstraction. I think it is a fair criticism of a language. Use sharp tools wisely.

(Disclaimer: writing Rust daily for work.)


Note that `impl Trait` in argument position has been stabilised and should be in a beta soon, and stable after that - so your latter example can be simplified to:

  trait Frobber {
    fn frob(&self, something: &impl OtherTrait);
  }


I am not tracking development besides release notes, but that's great news!


1.25 is released Thursday, and has some good stuff. This is coming in 1.26, and it and 1.27 are going to be monster releases with so so so much good stuff. Get ready :)


Just read that SIMD intrinsics may also be stabilized for 1.27. Keep up the good work!


> Erlang has an extremely unfamiliar syntax, but I don't think that makes it less suitable for building a service.

However, you can learn and understand all of Erlang's syntax in half a day.

Rust's syntax is... less than optimal.


I really don't understand people complaining about rust syntax.

Coming from a Java and JavaScript background, the Rust syntax was never a problem for me, it instantly felt like home (the only thing that bugged me was the lack or parenthesis around `if` expressions, and the lack of the `return` keyword at the end of a function but that's really no big deal and I internalized it pretty quickly). Some people complain about the angle bracket `<>` but that's from Java !

The type system can be a bit tricky to understand, for people not used to algebraic types, and the borrow checker and memory management story is hard to grasp, but the syntax, I just don't understand the complaints …


Syntax is literally the first thing people coming to a language see. Hence the complaints :)

If something clicks immediately for you ,might not click immediately (or at all) for others ;) However, Rust's potential syntax issues arise from the fact that they try to express complex concepts within a limited set of characters (and it's a problem any sufficiently complex language will face), so of course people will complain :)


Weird, for me it is exactly the other way around. The syntax might be familiar, but it is idiotic. On the other hand, the semantics is exactly what I have always wanted C++ to be.


>Having written services professionally in Java I have seen performance scale linearly with connections, until one day it suddenly spikes massively - why?

I'm going to guess: lack of load testing. Something I imagine you still need to do in Rust too.


> Having written services professionally in Java I have seen performance scale linearly with connections, until one day it suddenly spikes massively - why? It was that we crossed some threshold where the GC was suddenly having to do a ton more work, which caused a serious feedback loop - server has to pause, clients don't handle it well, clients hammer server more, server has to pause, etc.

Fair, but you almost never see things like this with Go, and when you do, it's pretty straightforward to triage since the GC and escape analysis are far simpler. Meanwhile, the performance is comparable.

> I find it far, far easier to write correct, expressive code in Rust.

I know this paragraph was comparing Rust to Java and not to Go, but while Rust is more expressive than Go, man does it take a long time to figure out how Rust wants you to express what you want to express. On the other hand, most things in Go do what you would expect, but sometimes you need to subvert the type system (which is a bummer, but it's worth it to me to sacrifice 1% of type safety for an order of magnitude or more productivity improvement).


How exactly is the Go GC easier to "triage" than that of the JVM?

Keep in mind that the HotSpot JVM does escape analysis just as Go does (this is sadly a very common misconception). The difference is that Go relies on escape analysis a lot more than the JVM does, because Go's GC has much slower allocation due to not being generational.

> man does it take a long time to figure out how Rust wants you to express what you want to express.

This goes away with experience. Certainly, most programmers get up to speed with Go faster than they do with Rust. But at this point the Rust language has totally melted away into the background for me.


> The difference is that Go relies on escape analysis a lot more than the JVM does, because Go's GC has much slower allocation due to not being generational.

True, but this is only a part of the story. In Java, every custom type is a reference type. This puts a lot of pressure on the GC because a lot of multiple small objects are created. Go has value types and doesn't suffer from this issue, which is a reason why having a generational GC is not that important for Go.


> This puts a lot of pressure on the GC because a lot of multiple small objects are created.

… but they are allocated in the nursery, which isn't comparable to an allocation in Go (by orders of magnitude). It only makes a difference if you have a long-lived object (which must be tenured) and the difference is noticeable only if the object is small enough to be memcopied multiple times at low cost.

That's why Go is only significantly faster than Java on the Go's marketing slides and not in real life.


I already agreed that allocation is faster in Java than in Go. What I'm saying is that allocation speed is less critical in Go than in Java.

In Java, if you allocate an object containing 10 other objects, you have to allocate 11 objects, because you only only have reference types (I know there is ongoing work to introduce value types). This is more work for the allocator, and for the GC. In Go, you allocate only once.

Honestly, I'm not sure about this issue being very significant for most programs in either Go or Java. As a reminder, most C, C++ and Rust programs don't use a bump allocator either and they're doing well.


> What I'm saying is that allocation speed is less critical in Go than in Java.

Indeed. In fact, Go couldn't afford not to have a generational GC without value type and Java would be really slow with Go's GC.

> As a reminder, most C, C++ and Rust programs don't use a bump allocator either and they're doing well.

C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.


> C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so.

It's actually very common to do this in performance oriented code. You allocate a decently sized temporary chunk of memory at the start of some period of work (say a frame of a game, or something like that) and then most/all temporary allocations are done from that in a bump pointer fashion. None of it get's freed, but in practice this isn't a big problem as long as you allocate a sufficiently large buffer (or handle overflow by allocating a second)

At the end of the period of work, you free the buffer. In the end you pay for a single malloc/free (and even this you can reduce if you re-use the buffer multiple times), despite having possibly many more allocations.

The downside is that you have some restrictions, you need to be sure none of the objects in the buffer outlives it, in C++ you probably want to ensure all the objects you allocate from it are trivially destructible, etc.

Either way, it's very common to do this sort of thing, at least in C and C++. That's partially because the system allocator is typically slow, but mostly because this sort of control is a big reason to use languages like this.


> C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.

Not true. You can use exactly the same techniques in C, C++ and Rust. It's called arena based allocation.

Even malloc calls can be very cheap. The Hoard allocator uses bump pointer allocation for malloc into empty pages!


> Go couldn't afford not to have a generational GC without value type

And Java couldn't afford not to have value types without a generational GC ;-)

> C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.

Agreed. This is why Go programmers use the same optimizations as C, C++ and Rust programmes is such a case (by allocating from a pool or an arena).

Are you aware of cases where what is gained thanks to the bump allocator is lost because of compaction?


> Agreed. This is why Go programmers use the same optimizations as C, C++ and Rust programmes is such a case (by allocating from a pool or an arena).

Yes, unfortunately in Go allocations are harder to avoid[1] than in C++ or Rust, because Go rely on escape analysis whereas objects must be manually boxed in the other languages.

[1]: https://groups.google.com/forum/#!topic/golang-nuts/Vcgx7hkh...


It's not "unfortunate". It's a known drawback of the tradeoff chosen by Go.


Which can be effectively mitigated by having a generational garbage collector!


> Go has value types and doesn't suffer from this issue, which is a reason why having a generational GC is not that important for Go.

Go is in the same camp as C#/.NET here, where the generational hypothesis certainly holds and therefore .NET uses a generational garbage collector. I am certain that the generational hypothesis holds for Go as well.

Not having a generational GC is a design mistake in Go. Google should fix that.


If you are interested by this topic, here is a relevant discussion on golang-nuts: https://groups.google.com/d/topic/golang-nuts/KJiyv2mV2pU/di...

One of the arguments mentioned against having a generational GC is that it would make concurrent GC with low latency harder (because Go permits interior pointers, unlike Java) and slower (because Go only needs write barrier, unlike Java which needs read barrier as far as I know).


Ian Lance Taylor is wrong in that thread. Interior pointers don't make anything harder: this is a solved problem with card marking. Indeed, .NET has interior pointers, and it doesn't cause any problem at all.

Also, I don't believe that the HotSpot GC needs to use read barriers. Read barriers are only necessary if you're concurrently compacting objects (like Azul C4 does). If you have concurrent mark-and-sweep and stop the world only during compaction phases, read barriers are unnecessary.


I didn't know about .NET permitting interior pointers. Thanks.

I think you're right about HotSpot GC not using read barriers. I was mixing it up with the Azul GC, which uses read barriers.

> If you have concurrent mark-and-sweep and stop the world only during compaction phases, read barriers are unnecessary.

Do you know if it's possible to bound the pause caused by the compaction phase to some ceiling, like 2 ms for example? I'm asking because it's a goal of Go' GC to limit GC pauses.


Here is an excerpt from the recent proposal on non-cooperative goroutine preemption [1], relevant to our discussion on GC and interior pointers:

> Many other garbage-collected languages use explicit safe-points on back-edges, or they use forward-simulation to reach a safe-point. Partly, it's possible for Go to support safe-points everywhere because Go's GC already must have excellent support for interior pointers; in many languages, interior pointers never appear at a safe-point. > [...] > Decoupling stack-move points from GC safe-points. [...] Such optimizations are possible because of Go's non-moving collector.

[1] https://github.com/golang/proposal/blob/master/design/24543-...


G1 has read barriers for SATB marking. Whether a read barrier is used or not is a function of which GC is used so can’t really say “Hotspot doesn’t use read barriers”.


Do you have a reference on G1 read barriers? I believe you, but I can't find any information about that from a Google search.

I don't immediately see why you'd need a read barrier for snapshot-at-the-beginning. You only need to trace references from gray objects to white objects that were deleted, which is a write.


Sorry, that was my mistake - it has pre-write barriers for SATB but not actual read barriers.

However, ZGC will have read barriers and if Shenandoah ever gets integrated into Hotspot, it has them too.


> Indeed, .NET has interior pointers, and it doesn't cause any problem at all.

Are you sure about this (without using C# unsafe context)? I've spent a moment digging in C# documentation and been unable to confirm this.


Yes. The "ref" keyword allows taking interior pointers to data members as function arguments [1]. There are also local references [2].

[1]: https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...

[2]: https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...


.NET doesn’t have interior pointer. Any `ref` must be on the stack and that’s tracked in the stackmap. You cannot have a ref as a field.


.NET doesn't have heap interior pointers (today), but that doesn't matter for this argument. You still need to be able to mark objects as live even if they're only referenced by interior pointers.


> .NET doesn't have heap interior pointers (today), but that doesn't matter for this argument.

I think it matters for this argument.

You are arguing that designing a garbage collector which is concurrent, low latency (pauses < 5 ms), compacting, generational, and supports interior pointers, is easy.

You mentioned C#/.NET as an example, but C# doesn't have interior pointers (from heap to heap, not from stack to heap which is possible).

As far as I know, neither Java, .NET, Go, Haskell, OCaml, D, V8 or Erlang satisfies all these requirements at the same time.

I'm not saying it's impossible, and Ian Lance Taylor in the thread I linked earlier is not saying either. I'm just saying it's certainly hard.

If it would be so easy, then most languages would already have interior pointers and a concurrent, low latency, compacting, generational GC. That's the whole difference between "today" (as in your comment) and "tomorrow".


> In Java, every custom type is a reference type.

I'm not sure this is true in the actual native code produced by the JIT. HotSpot does autobox elimination.


No, it’s sadly true. EA may scalarize the allocation but this optimization falls apart very easily in Hotspot.


> How exactly is the Go GC easier to "triage" than that of the JVM?

Well, firstly the language has value semantics, so in many cases you can read the source code knowing nothing about escape analysis and make inferences about where data lives. With Java, you're at the mercy of escape analysis. Further, Go's escape analysis seems simpler; I've been programming in Java on and off for a decade and I depend on profiling to hunt down allocations (except in very simple cases). This isn't the case in Go despite working with it for only a few years recreationally.

> This goes away eventually

I keep hearing that, but I'm 4 years into Rust and I'm still not able to be reasonably productive. I'm sure it's possible to get there, but I doubt I will ever be as productive in Rust as I am in Go for the class of applications I write.


> Well, firstly the language has value semantics, so in many cases you can read the source code knowing nothing about escape analysis and make inferences about where data lives.

Are you referring to heap vs. stack? That is not as simple as it seems, because of escape analysis. The only thing that Go lets you do that Java doesn't is put structs inside other structs, which is nice, but it's not a game-changer.

> Further, Go's escape analysis seems simpler; I've been programming in Java on and off for a decade and I depend on profiling to hunt down allocations (except in very simple cases). This isn't the case in Go despite working with it for only a few years recreationally.

I don't see how the HotSpot JVM escape analysis can be "simpler" than that of Go. They work the same way.


> Are you referring to heap vs. stack? That is not as simple as it seems, because of escape analysis. The only thing that Go lets you do that Java doesn't is put structs inside other structs, which is nice, but it's not a game-changer.

This isn't true. I can get a contiguous slice of structs, and I can pass value types by copy without needing to reason about escape analysis at all. If I do `var foo Foo` (assuming Foo is a struct type), I know that `foo` won't escape unless I return a pointer to it (or to something in it), at which point it's subject to escape analysis. I can get quite a long way in Go without needing to reason about escape analysis at all.

> I don't see how the HotSpot JVM escape analysis can be "simpler" than that of Go. They work the same way.

I'm pretty sure they don't work the same way, but I don't have any examples off hand to share (it's been a while since I worked with Java). I think Java's escape analysis is more sophisticated.


> Fair, but you almost never see things like this with Go, and when you do, it's pretty straightforward to triage since the GC and escape analysis are far simpler.

The Go GC is not a panacea. It trades lower STW pause times for a huge drop in throughput under GC pressure and simplicity for terrible performance with large, mostly static heaps when compared to a generational collector.

This entire discussion and the original article is worth reading: https://www.reddit.com/r/programming/comments/5fyhjb/golangs...


Agreed. My point is that those tradeoffs are really, really good for a huge swath of applications.


I'm not sure that's true. It works well for web proxies and simple CRUD apps. Anything which needs a decently sized heap is multiple times slower than Java, OCaml etc.

CockroachDB only works at all because the actual DB is in RocksDB written in C++.


I don't know what constitutes "decently sized", but I've not had significant issues with a 12GB heap.


I have the opposite experience, Go's is pretty bad at reclaiming memory (I think that's the trade-of for not having long pauses), and we've been been bitten by lack of memory in production a few times. The solutions are :

1. allocate less, but this is not always straightforward since Go doesn't give you a fine control on memory allocation (compared to Rust). 2. change the VM tier to get more memory, what we eventually did.


It's pretty easy to intuit about where allocations will occur in Go even though it's not explicit as in Rust, but you don't even have to intuit because Go's profiler does an excellent job of highlighting the allocations, and strategies for working around them are the same as in Rust.

The profiling bit is harder than in Rust, but you only need to do this analysis in the hot path, while Rust requires you to make memory allocation decisions everywhere.


Well Rocket is about as verbose as express:

#[get("/<name>/<age>")]

fn hello(name: String, age: u8) -> String {

    format!("Hello, {} year old named {}!", age, name)

}


I'm personally a big advocate of Rust. That said, like yourself, I'm also a little skeptical that it's the right tool for most web apps. I think a case can be made for small Rust web apps embedded in consumer electronics that may be very resource constrained, though. (Granted, these probably aren't the sorts of apps that would be using PostgreSQL as described in the article.) I've seen a lot of great web applications written in Go that can also be quite compact (compared to Java, for instance), and I think Go might be a great candidate for the next level up in resource availability.

Like many languages, the syntax becomes a lot more clear after you've used the language a while. Maybe it's me who's been warped, but I've been programming in Rust for long enough that the example you pasted looks perfectly normal. ;)


You can write an overcomplicated framework in any language. Some of those Java frameworks are pretty impenetrable too.

Here's an example of some Rust code that's even faster and is short and sweet: https://github.com/tokio-rs/tokio-minihttp/blob/master/examp...

(I don't think the TechEmpower benchmarks really mean much, incidentally. They're so simple that they're way too easy to game.)


The benchmarks establish a high-water mark and establish the magnitude of performance. Specific rank ordering is mostly a trivial matter, if admittedly a point of potentially fun competition.

Some laser-focus on the plaintext benchmark in particular, presumably because it's the easiest to implement and the delightfully high numbers in the results are exciting. But more interesting are the test types that involve round-trips to the database, using an ORM (or similar), and other framework features. For me, the Fortunes test is the most interesting.

Yes, it's "easy" to climb up a few positions and jockey for specific rank order against other similar contenders, but considering specific rank ordering is not a useful way to interpret the results. More useful is to see the orders of magnitude conveyed by the results. And it is not so easy for a low-performance platform (e.g., Ruby) to game its way into the top positions. See, for example, Java, Rust, and Ruby on the Fortunes test:

https://www.techempower.com/benchmarks/#section=data-r15&hw=...

Obviously, no small benchmark can measure the performance characteristics of your application as implemented on multiple frameworks. But these are a proxy, and illustrate the performance landscape your application will exist within. They can reveal the bottlenecks your application will be unable to avoid in framework services such as JSON serialization, object-relational conversion, connection pooling, server-side templates, and so on.


Sure, but by the same token, you can't conclude Rust code is never faster than Go/Java/whatever for real-world code on account of some TechEmpower benchmark, as the parent poster was trying to do.


> Sure, but by the same token, you can't conclude Rust code is never faster than Go/Java/whatever for real-world code on account of some TechEmpower benchmark, as the parent poster was trying to do.

A very good point, and I had overlooked what the parent poster had said!

Rust is clearly part of the top performance category and I would not be surprised if with some work it were to become (momentarily) faster by some small amount than Java, Go, etc. I say "momentarily" here because many of the performance-oriented frameworks and platforms are continuously working on tuning within their stacks, so there is considerable volatility in the specific ordering over time.


They can also be establishing a minimum performance gain.

For Ruby, most of the expensive parts of shuffling around HTTP requests, fetching data from a DB, and serializing JSON, is done using libraries with native extensions. This means using a lightweight framework and ORM like Sinatra + Sequel can put you within striking distance (~3x slower) than Go + Gin on benchmarks like the TechEmpower benchmarks.

However, if you do something with multiple passes of tight loops over the data before you return it, Go will pull much further ahead.


> Rust is a good language but I don't see it any time soon in the web space with that syntax and the lack of mature libraries, plus the time it takes to get anything done.

Lack of mature libraries is always a problem, syntax is subjective and "time it takes go get anything done" .. which time do you measure? Time-to-first-running-example or Time-to-debugged-productions-ready-code? I have my doubts that Rust takes significantly longer for the second one.


>which time do you measure? Time-to-first-running-example or Time-to-debugged-productions-ready-code?

Had the same experience. Rust feels slower to develop, but the type system just saves you so much headache in the long run that you're usually much faster at reaching your goal.

Lack of mature libraries is a normal issue for a language that's 3 years old. It will get better with use.


The syntax is not that bad but there are some poor variable names that make the code more difficult to grasp.

The motivation to learn a new language and the excitement during the process are huge factors that will push many people to write a web service in Rust.


To learn Rust?

Also, where do you get that the performance is the same? I didn't see any benchmarks.


In the linked article by the OP Java is faster than Rust in every scenarios.

https://www.techempower.com/benchmarks/#section=data-r15&hw=...

tbh benchmarks are not that useful, but it shows that GC languages are very fast in the web space and it's even more true when you add client -> server latency in the mix.


The benchmark you linked has also latency tab. Try to press it. Looks like Rust libraries deliver very good latency with low jitter.


Heh, I bet rust also wins on memory usage by a land-slide.


Not in Plaintext. But yes, Java has some extremely optimized libraries for HTTP, and a great GC has a lot of advantages for this sort of benchmark I'd imagine (short lived allocations, deferred collections).

That said, actix is particularly new, with room to optimize.


People often forget about the perf benefits of GC, focusing only on the overhead of collection. Still, you might be able to win in rust by using something like arenas when possible (though the type state system might not help in that case).


Rust hasn't had typestate in something like four years now.

Arenas and lifetimes work well together; the compiler actually has a hierarchy of arenas inside of it, and makes sure thanks to lifetimes that once the shorter-lived ones are freed, they don't get used again, for example.


(Maybe Steve is around)

Why did Rust go with the IMHO ugly closure syntax of

    |stuff|
?

I think that

     stuff ->
is much nicer to look at... :(

Also, not presented here, but ‘ is also kind of ugly, was there no short keyword that could have been used? “life or “lt” for “lifetime”? “span”?


That closure syntax has been around longer than Steve.

So I take it you would like to be able to declare your references as `&life of brian`?


Heh, who knows, maybe Python adds this feature for Python 4 :p


As my sibling says, it was there before me. Ruby and smalltalk also use this syntax, so it’s not without precedence.

As for lifetimes, that syntax is from OCaml, though they use it for something else. “life” and “lt” seem too short, “span” is overloaded. We tried to explore options but none were clearly better, so we kept what we had.


> As for lifetimes, that syntax is from OCaml, though they use it for something else.

I hadn't noticed the similarity but it makes sense. This is the OCaml syntax for generics, eg:

    type 'a list_with_generic = 'a list;


Yeah, it's also not as different as it sounds, as lifetimes are fundamentally generics, just of a different kind than types.


Rust code is 80% technicalities and only 20% actual domain.


I've been using 'actix-web' in anger for the last month or so and I just want to echo that it is an amazingly fast and fully-featured project.

I smiled when reading this post because I absolutely recognize the euphoria of carrying out a world-changing refactor and having it work first time. If this is what the future of backend development feels like, sign me up!


Have you look at rocket etc... Curious about your decision process.


I like rocket a lot - practically as easy as writing flask but with glorious type-safety thrown in - but I really wanted async ('like, now') because my project will have a large number of persistent connections. I'm sure rocket will get async eventually but it looks to be some way off.

In comparison to rocket, actix-web does not use codegen tricks so is not as ergonomic or as safe - e.g. it cannot statically check that the path variables that you ask for actually exist on the route - but these are things that a simple test suite easily picks up (incidentally actix-web has a nice built-in test server).

Also I was a little concerned that rocket development has stalled somewhat. 26 PRs waiting at time of writing. As I recall the maintainer is teaching a Rust class as Stanford so he's most likely just very busy.


i just added extractors system. it is not released yet, i am planing to release it later this week

https://actix.github.io/actix-web/actix_web/struct.Route.htm...


For async, also worth considering Gotham: https://gotham.rs/

I only have minimal experience with it, since I've also eventually chosen actix-web for all my projects so far due to its simplicity.


Really nice article.

I’ve been using rust moderately for the last 6 months and at least my experince was that once you get used to playing along with the compiler and understand the fundamental features of the language, it almost feels too good to be true.

Perfect mix of high performance, safety and modern features. Slightly challenging at first, but it really is worth the initial pain.


[flagged]


:D

At least I didn't. It was the first time I commented on Rust here on HN.


The author briefly mentions Haskell but I wonder whether they've build web services with it, or with another (loosely) ML-family language (e.g. OCaml, F#, or my personal favourite Scala). An ML-style type system is a huge advantage over Ruby/JavaScript/..., but for a web service I struggle to see a real case for going to all the effort of memory ownership tracking rather than using a GCed language with the same kind of strong type system; it would be good to see a comparison from someone who's done both.


I think that despite the constraints imposed by the lack of GC, Rust is seen as more approachable than more functional langauges to those coming from a C/Java/JavaScript etc background. And the only similar language with an ML type system is Swift which isn't quite there with libraries for backend stuff.

Also, have you seen the sample code for Rocket based webapps? I haven't seen much nicer in any language. The async ecosystem isn't nearly as nice yet, but it should get much nicer once async-await lands.


> Also, have you seen the sample code for Rocket based webapps? I haven't seen much nicer in any language.

Had a quick look just now. Clean syntax for the implementation, but routing and parameters are relying on magic # annotations. Compare with e.g. https://doc.akka.io/docs/akka-http/current/routing-dsl/index... where you can do the routing in plain old code (also async is already there).


I love me some akka. Actix would be the actual rust comparison, since it's also an actor system being leveraged to provide web features. You have to dig into Tokio/futures to get access to streams, which atm is still not at 1.0.


I'm not comparing it as an "actor thing", I'm comparing it as a server-side HTTP framework; the part I care about is not whether the implementation uses actors underneath, but whether the routing and logic can be expressed in plain old code that can be refactored according to the normal rules of the language, with full type safety, with the ability to handle effect-like types...

(I hate akka and actors, but as a user of akka-http you don't have to touch them.)


> routing and parameters are relying on magic # annotations

Actually, as someone almost finished with the book, that brings up an interesting point. What controls # annotations and how do you hook into them (which also answers the question of how magic they are)?

If it's fairly standard and obvious how to look up, I think I might not classify them as much "magic", but truthfully, I have no idea at all. Maybe I'm just not remembering that part of the book (I did take a break for a month), or maybe I didn't make a connection that was implied?


Look into macros 1.1 to see how they're use for `#derived` they're not particularly super magical. Though unstable stuff rocket uses is a bit more magical.


Right now, those annotations (attributes in Rust parlance) are only made available by the compiler, but this year, the feature Rocket uses to define your own will be stable. You write a function that takes the code as input and produces code as output, to put it in the simplest of terms.


> It’d be fair to say that I could’ve written an equivalent service in Ruby in a tenth of the time it took me to write this one in Rust.

Ha, I had the same feeling when I needed to create a simple REST service that'd just process a request using command line tools. Seems easy but Rust's tooling is full of rough edges. Code completion sometimes works sometimes doesn't, cargo cannot install dependencies only (without building source) so it's not good for Dockerfile layers, etc. etc. Borrow checker is not bad at all for people that worked with languages with explicit memory management (where you do ownership tracking in your head anyway).

Long story short I spent 2 days working on the service and had 80% done but figured out the rest would take twice as much if I want this to be production quality. I scraped the project and rewritten it in node, using one or two dependencies in 2 hours.

I'll be trying Rust again surely and I'm glad that articles like this one exist!


Very exciting to see Rust gaining support lately.

That said, one of the only remaining things keeping me from using it is production ready gRPC library.


Just an FYI (if you want something to watch and haven't heard about it already) https://github.com/tower-rs/tower-grpc is in the works by the same folks building tokio and hyper etc.

But yep, definitely not production ready as the README clearly states :)


I would say that you could start using it if:

* You are ready to become an active contributor to the project.

* You are brave!


Thank you for writing/posting this!

Specifically the custom bindings using Diesel. I was unaware that generic sql could be bound so easily. That’s closer to how I end up when needing to create higher performance queries. I need to take another look at Diesel now.

Very nice article.


Thanks for writing this. Actix is really cool but I haven't really spent the time looking into it - I was unaware of the SyncArbiter abstraction.

This is a very helpful post for me.


I love rust, but to be honest, until the nightmare that is tokio/futures is fixed with native async/await and better compiler error messages, a strongly typed language like C# with those features natively present is my choice for web services. It addresses all the author’s issues with Ruby and JS, and is still orders of magnitude faster than those options (though admittedly not as fast as a C or rust option).


Just wanted to mention that your usage of `sql_query` is exactly how it's meant to be used, and exactly where I would reach for it. Great article!


tldr; - it was hard for me to determine which rust web framework I should be using, since I want something light like flask. best resource was https://github.com/flosse/rust-web-framework-comparison

Very recently I've taken a tour around the Rust web server ecosystem, and I think it's way too hard to find one to pick.

The author mentions actix-web[0], and mentions how fast it is, but it's actually right below hyper[1], which I find to be simpler, because it doesn't bring in the whole actor frame work thing. Hyper builds on Tokio[2] which introduces the usual event loop paradigm that we're used to in other languages (which AFAIK is the fastest way to write web servers to date). There's also great talk[3] that introduces the choices that tokio makes that are somewhat unique.

Here's what I want out of a web framework:

- express like-interface (func(req,resp,next) pattern provies just the right amount of freedom/structure IMO)

- good routing (plus: decorators/a fairly ergonomic way to specify routes)

- reasonable higher-level interfaces to implement (I want to implement an interface, and be able to receive things like logging, tracing, grpc/thrift/whatever transports for free, good interfaces are what make writing reusable stuff like that possible)

Here's what I found about the various frameworks that exist out there:

- Rocket.rs[4]: Seems to do too much (I tend towards building APIs), rocket is closer to django/rails than it is to flask/sinatra.

- Gotham.rs[5]: Seems perfect, but falls flat on the feature front, half the stuff on the landing page are features of rust, not the library. Doesn't seem to have enough batteries included, for example there's currently work being done on streaming request bodies (https://github.com/gotham-rs/gotham/issues/189), that's not a issue I want to run into.

- Iron.rs[6]: The oldest of the bunch, but also very very fast (which I discovered actually browsing rocket's issues[7]), not based on hyper, and also not actively maintained.

I had no idea which of these to use, or how to find ones I've missed, then I stumbled upon this amazing resource: https://github.com/flosse/rust-web-framework-comparison.

However, when you look at that comparison, it's really suspicious how much of actix-web has filled out, which is often indicative of something written from one point of view (like when people have comparison pages, and one option seems to just have "everything"). And again, like I said actix seems to be doing too much, I don't really want to utilize the actor model, I just want a relatively fast, ergonomic simple library with the most basic batteries included.

BTW, everyone keeps raving about type safety and I wonder why people don't give Haskell more of a go. If Rust gives you the type safety equivalent to a shield, haskell is a phalanx. I've never felt more safe and protected by my types than when I write Haskell -- it's relatively fast, memory safe, has fantastic concurrency primitives, and though it can be tough to optimize (which comes to down to optimizing for lower amounts of GCs like most other memory managed languages), it's pretty fast out of the gate. I use servant (https://haskell-servant.readthedocs.io/) and love it.

[0]: https://github.com/actix/actix-web

[1]: https://hyper.rs/

[2]: https://tokio.rs/

[3]: https://www.youtube.com/watch?v=4QZ0-vIIFug

[4]: https://rocket.rs/

[5]: https://gotham.rs/

[6]: http://ironframework.io/

[7]: https://github.com/SergioBenitez/Rocket/issues/552


Wow, thanks a lot for this. It's definitely helpful, especially the link to the comparison which has a lot of resources and examples.


Iron does use hyper, but it uses an older, synchronous version of hyper. Iron also isn't actively maintained.


Thanks for the clarification -- I was wondering what iron was using under the covers but didn't care enough to down and look.

With the numbers iron puts up, I would imagine that it's actually doing an event loop somewhere, though, it's way too fast to not be.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: