Rust vs Python: Rust will not replace Python

I love Python, I used it for 10+ years. I also love Rust, I have been learning it for the last year. I wanted a language to replace Python, I looked into Go and became disappointed. I’m excited about Rust, but it’s clear to me that it’s not going to replace Python.

In some parts, yes. There are small niches where Rust can be better than Python and replace it. Games and Microservices seem ones of the best candidates, but Rust will need a lot of time to get there. GUI programs have also a very good opportunity, but the fact that Rust model is too different from regular OOP makes it hard to integrate with existing toolkits, and a GUI toolkit is not something easy to do from scratch.

On CLI programs and utilities, Go is probably to prevent Rust from gaining some ground here. Go is clearly targeted towards this particular scenario, is really simple to learn and code, and it does this really well.

What Python lacks

To understand what are the opportunities from other languages to replace Python we should first look to what are the shortfalls of Python.

Static Typing

There are lots of things that Python could improve, but lately I feel that types are one of the top problems that need to be fixed, and it actually looks it’s fixable.

Python, like Javascript, is completely not typed. You can’t easily control what are the input and output types of functions, or what are the types of local variables.

There’s the option now to type your variables and check it with programs like MyPy or PyType. This is good and a huge step forward, but insufficient.

When coding, having IDE autocompletion, suggestions and inspection helps a lot when writing code, as it speeds up the developer by reducing round-trips to the documentation. On complex codebases it really helps a lot because you don’t need to navigate through lots of files to determine what’s the type that you’re trying to access.

Without types, an IDE is almost unable to determine what are the contents of a variable. It needs to guess and it’s not good. Currently, I don’t know of any autocompletion in Python solely based on MyPy.

If types were enforced by Python, then the compiler/interpreter could do some extra optimizations that aren’t possible now.

Also, there’s the problem of big codebases in Python with contributions of non-senior Python programmers. A senior developer will try to assume a “contract” for functions and objects, like, what are the “valid” inputs for that it works, what are valid outputs that must be checked from the caller. Having strict types is a good reminder for not so experienced people to have consistent designs and checks.

Just have a look on how Typescript improved upon JavaScript by just requiring types. Taking a step further and making Python enforce a minimum, so the developer needs to specify that doesn’t want to type something it will make programs easier to maintain overall. Of course this needs a way to disable it, as forcing it on every scenario would kill a lot of good things on python.

And this needs to be enforced down to libraries. The current problem is that a lot of libraries just don’t care, and if someone wants to enforce it, it gets painful as the number of dependencies increase.

Static analysis in Python exists, but it is weak. Having types enforced would allow to better, faster, and more comprehensive static analysis tools to appear. This is a strong point in Rust, as the compiler itself is doing already a lot of static analysis. If you add other tools like Cargo Clippy, it gets even better.

All of this is important to keep the codebase clean and neat, and to catch bugs before running the code.

Performance

The fact that Python is one of the slowest programming languages in use shouldn’t be news to anyone. But as I covered before in this blog, this is more nuanced than it seems at first.

Python makes heavy use of integration with C libraries, and that’s where its power unleashes. C code called from Python is still going at C speed, and while that is running the GIL is released, allowing you to do a slight multithreading.

The slowness of Python comes from the amount of magic it can do, the fact that almost anything can be replaced, mocked, whatever you want. This makes Python specially good when designing complex logic, as it is able to hide it very nicely. And monkey-patching is very useful in several scenarios.

Python works really well with Machine Learning tooling, as it is a good interface to design what the ML libraries should do. It might be slow, but a few lines of code that configure the underlying libraries take almost zero time, and those libraries do the hard work. So ML in Python is really fast and convenient.

Also, don’t forget that when such levels of introspection and “magic” are needed, regardless of the language, it is slow. This can be seen when comparing ORMs between Python and Go. As soon as the ORM is doing the magic for you, it becomes slow, in any language. To avoid this from happening you need an ORM that it’s simple, and not that automatic and convenient.

The problem arises when we need to do something where a library (that interfaces C) doesn’t exist. We end coding the actual thing manually and this becomes painfully slow.

PyPy solves part of the problem. It is able to optimize some pure python code and run it to speeds near to Javascript and Go (Note that Javascript is really fast to run). There are two problems with this approach, the first one is that the majority of python code can’t be optimized enough to get good performance. The second problem is that PyPy is not compatible with all libraries, since the libraries need to be compiled against PyPy instead of CPython.

If Python were stricter by default, allowing for wizardry stuff only when the developer really needs it, and enforcing this via annotations (types and so), I guess that both PyPy and CPython could optimize it further as it can do better assumptions on how the code is supposed to run.

The ML libraries and similar ones are able to build C code on the fly, and that should be possible for CPython itself too. If Python included a sub-language to do high-performance stuff, even if it takes more time to start a program, it would allow programmers to optimize the critical parts of the code that are specially slow. But this needs to be included on the main language and bundled on every Python installation. That would also mean that some libraries could get away with pure-python, without having to release binaries, which in turn, will increase the compatibility of these with other interpreters like PyPy.

There’s Cython and Pyrex, which I used on the past, but the problem on these is that it will force you to build the code for the different CPU targets and python versions, and that’s hard to maintain. Building anything on Windows is quite painful.

The GIL is another front here. By only allowing Python to execute a instruction at once, threads cannot be used to distribute pure python CPU intensive operations between cores. Better Python optimizations could in fact relief this by determining that function A is totally independent of function B, and allowing them to run in parallel; or even, they could build them into non-pythonic instructions if the code clearly is not making use of any Python magic. This could allow for the GIL to be released, and hence, parallelize much better.

Python & Rust together via WASM

This could solve great part of the problems if it works easy and simple. WebAssembly (WASM) was thought as a way to replace Javascript on browsers, but the neat thing is that creates code that can be run from any programming language and is independent of the CPU target.

I haven’t explored this myself, but if it can deliver what it promises, it means that you only need to build Rust code once and bundle the WASM. This should work on all CPUs and Python interpreters.

The problem I believe it is that the WASM loader for Python will need to be compiled for each combination of CPU, OS and Python interpreter. It’s far from perfect, but at least, it’s easier to get a small common library to support everything, and then other libraries or code to build on top of it. So this could relief some maintenance problems from other libraries by diverting that work onto WASM maintainers.

Other possible problem is that WASM will have it hard to do any stuff that it’s not strictly CPU computing. For example, if it has to manage sockets, files, communicate with the OS, etc. As WASM was designed to be run inside a browser, I expect that all OS communication would require a common API, and that will have some caveats for sure. While the tasks I mentioned before I expect them to be usable from WASM, things like OpenGL and directly communicating with a GPU will surely have a lack of support for long time.

What Rust Lacks

While most people will think that Rust needs to be easier to code, that it is a complex language that it requires a lot of human hours to get the code working, let me heavily disagree.

Rust is one of the most pleasant languages to code on when you have the expertise on the language. It is quite productive almost on the level of Python and very readable.

The problem is gaining this expertise. Takes way too much effort for newcomers, especially when they are already seasoned on dynamic-typed languages.

An easier way to get started in Rust

And I know that this has been said a lot by novice people, and it has been discussed ad-infinitum: We need a RustScript language.

For the sake of simplicity, I named RustScript to this hypothetical language. To my knowledge, this name is not used and RustScript does not exist, even if I sound like it does.

As I read about others proposing this, please keep reading as I already know more or less what has been proposed already and some of those discussions.

The main problem with learning Rust is the borrow-checking rules, (almost) everyone knows that. A RustScript language must have a garbage collector built in.

But the other problem that is not so talked about is the complexity of reading and understanding properly Rust code. Because people come in, try a few things, and the compiler keeps complaining everywhere, they don’t get to learn the basic stuff that would allow them to read code easily. These people will struggle even remembering if the type was f32, float or numeric.

A RustScript language must serve as a bootstrapping into Rust syntax and features of the language, while keeping the hard/puzzling stuff away. In this way, once someone is able to use RustScript easily, they will be able to learn proper Rust with a smaller learning curve, feeling familiar already, and knowing how the code should look like.

So it should change this learning curve:

Into something like this:

Here’s the problem: Rust takes months of learning to be minimally productive. Without knowing properly a lot of complex stuff, you can’t really do much with it, which becomes into frustration.

Some companies require 6 months of training to get productive inside. Do we really expect them also to increase that by another 6 months?

What it’s good about Python it’s that newcomers are productive from day zero. Rust doesn’t need to target this, but the current situation is way too bad and it’s hurting its success.

A lot of programming languages and changes have been proposed or even done but fail to solve this problem completely.

This hypothetical language must:

  • Include a Garbage Collector (GC) or any other solution that avoids requiring a borrow checker.
    Why? Removing this complexity is the main reason for RustScript to exist.
  • Have almost the same syntax as Rust, at least for the features they have in common.
    Why? Because if newcomers don’t learn the same syntax, then they aren’t doing any progress towards learning Rust.
  • Binary and Linker compatible with Rust; all libraries and tooling must work inside RustScript.
    Why? Having a complete different set of libraries would be a headache and it will require a complete different ecosystem. Newcomers should familiarize themselves with Rust libraries, not RustScript specific ones.
  • Rust sample code must be able to be machine-translated into RustScript, like how Python2 can be translated into Python3 using the 2to3 tool. (Some things like macro declarations might not work as they might not have a replacement in RustScript)
    Why? Documentation is key. Having a way to automatically translate your documentation into RustScript will make everyone’s life easier. I don’t want this guessing the API game that happens in PyQT.
  • Officially supported by the Rust team itself, and bundled with Rust when installing via RustUp.
    Why? People will install Rust via RustUp. Ideally, RustScript should be part of it, allowing for easy integration between both languages.

Almost any of these requirements alone is going to be hard to do. Getting a language that does everything needed with all the support… it’s not something I expect happening, ever.

I mean, Python has it easier. What I would ask to Python is way more realizable that what I’m asking here, and yet in 10 years there’s just slight changes in the right direction. With that in mind, I don’t expect Rust to ever have a proper RustScript, but if it happens, well, I would love to see it.

What would be even better is that RustScript were almost a superset of Rust, making Rust programs mostly valid in RustScript, with few exceptions such as macro creation. This would allow developers to incrementally change to Rust as they see fit, and face the borrow checker in small amounts, that are easy to digest. But anyway, having to declare a whole file or module as RustScript would still work, as it will allow devs to migrate file by file or module by module. That’s still better than having to choose between language X or Y for a full project.

Anyway, I’d better stop talking about this, as it’s not gonna happen, and it would require a full post (or several) anyways to describe such a language.

Proper REPL

Python is really good on it’s REPL, and a lot of tools make use of this. Rust REPL exist, but not officially supported, and they’re far from perfect.

A REPL is useful when doing ML and when trying out small things. The fact that Rust needs to compile everything, makes this quite useless as it needs boilerplate to work and every instruction takes time to get built interactively.

If Rust had a script language this would be simpler, as a REPL for scripting languages tends to be straightforward.

Simpler integration with C++ libraries

Given that both Rust and Python integrate only with C and not C++ would make anyone think that they are on the same level here; but no. Because Python’s OOP is quite similar to C++ and it’s magic can make for the missing parts (method overloading), in the end Python has way better integration with C++ than Rust.

There are a lot of ongoing efforts to make C++ integration easier in Rust, but I’m not that sure if they will get at any point something straightforward to use. There’s a lot of pressure on this and I expect it to get much, much better in the next years.

But still, the fact that Rust has strict rules on borrowing and C++ doesn’t, and C++ exceptions really don’t mix with anything else in Rust, it will make this hard to get right.

Maybe the solution is having a C++ compiler written in Rust, and make it part of the Cargo suite, so the sources can be copied inside the project and build the library for Rust, entirely using Rust. This might allow some extra insights and automation that makes things easier, but C++ is quite a beast nowadays, and having a compiler that supports the newest standards is a lot of work. This solution would also conflict with Linux distributions, as the same C++ library would need to be shipped twice in different versions, a standard one and a Rust-compatible one.

Lack of binary libraries and dynamic linking

All Rust dependencies currently rely on downloading and building the sources for each project. Because there so many dependencies, building a project takes a long time. And distributing our build means installing a big binary that contains everything inside. Linux distributions don’t like this.

Having pre-built libraries for common targets it would be nice, or if not a full build, maybe a half-way of some sort that contains the most complex part done, just requiring the final optimization stages for targeting the specific CPU; similar to what WASM is, *.pyc or the JVM. This would reduce building times by a huge amount and will make development more pleasant.

Dynamic linking is another point commonly overlooked. I believe it can be done in Rust but it’s not something that they explain on the regular books. It’s complex and tricky to do, where the regular approach is quite straightforward. This means that any update on any of your libraries require a full build and a full release of all your components.

If an automated way existed to do this in Cargo, even if it builds the libraries in some format that can’t be shared across different applications, it could already have some benefits from what we have. For example, the linking stage could take less time, as most of the time seems to be spent trying to glue everything together. Another possible benefit is that as it will produce N files instead of 1 (let’s say 10), if your application has a way to auto-update, it could update selectively the files needed, instead of re-downloading a full fat binary.

To get this to work across different applications, such as what Linux distributions do, the Rust compiler needs to have better standards and compatibility between builds, so if one library is built using rustc 1.50.0 and the application was built against 1.49.0, they need to work. I believe currently this doesn’t work well and there are no guarantees for binary compatibility across versions. (I might be wrong)

On devices where disk space and memory is constrained, having dynamic libraries shared across applications might help a lot fitting the different projects on such devices. Those might be microcontrollers or small computers. For our current desktop computers and phones, this isn’t a big deal.

The other reason why Linux distributions want these pieces separated is that when a library has a security patch, usually all it takes is to replace the library on the filesystem and you’re safe. With Rust applications you depend on each one of the maintainers of each project to update and release updated versions. Then, a security patch for an OS instead of being, say, 10MiB, it could be 2GiB because of the amount of projects that use the same library.

No officially supported libraries aside of std

In a past article Someone stop NodeJS package madness, please!!, I talked about how bad is the ecosystem in JavaScript. Because everyone does packages and there’s no control, there’s a lot of cross dependency hell.

This can happen to Rust as it has the same system. The difference is that Rust comes with “std”, which contains a lot of common tooling that prevents this from getting completely out of hand.

Python also has the same in PyPI, but turns out that the standard Python libraries cover a lot more functionality than “std”. So PyPI is quite saner than any other repository.

Rust has its reasons to have a thin std library, and probably it’s for the best. But something has to be done about the remaining common functionality that doesn’t cover.

There are lots of solutions. For example, having a second standard library which bundles all remaining common stuff (call it “extra_std” or whatever), then everyone building libraries will tend to depend on that one, instead of a myriad of different dependencies.

Another option is to promote specific libraries as “semi-official”, to point people to use these over other options if possible.

The main problem of having everyone upload and cross-depend between them is that these libraries might have just one maintainer, and that maintainer might move on and forget about these libraries forever; then you have a lot of programs and libraries depending on it unaware that it’s obsolete from long ago. Forking the library doesn’t solve the problem because no one has access to the original repo to say “deprecated, please use X”.

Another problem are security implications from doing this. You depend on a project that might have been audited on the past or never, but the new version is surely not audited. In which state is the code? Is it sound or it abuses unsafe to worrying levels? We’ll need to inspect it ourselves and we all know that most of us would never do that.

So if I were to fix this, I would say that a Rust committee with security expertise should select and promote which libraries are “common” and “sane enough”, then fork them under a slightly different name, do an audit, and always upload audited-only code. Having a group looking onto those forked libraries means that if the library is once deprecated they will correctly update the status and send people to the right replacement. If someone does a fork of a library and then that one is preferred, the security fork should then migrate and follow that fork, so everyone depending on it is smoothly migrated.

In this way, “serde” would have a fork called something like “serde-audited” or “rust-audit-group/serde”. Yes, it will be always a few versions behind, but it will be safer to depend on it than depending on upstream.

No introspection tooling in std

Python is heavy on introspection stuff and it’s super nice to automate stuff. Even Go has some introspection capabilities for their interfaces. Rust on the other hand needs to make use of macros, and the sad part is that there aren’t any officially supported macros that makes this more or less work. Even contributed packages are quite ugly to use.

Something that tends to be quite common in Python is iterating through the elements of a object/struct; their names and their values.

I would like to see a Derive macro in std to add methods that are able to list the names of the different fields, and standardize this for things like Serde. Because if using Serde is overkill for some program, then you have to cook these macros yourself.

The other problem is the lack of standard variadic types. So if I were to iterate through the values/content of each field, it becomes toilsome to do and inconvenient, because you need to know in advance which types you might receive and how, having to add boilerplate to support all of this.

The traits also lack some supertraits to be able to classify easily some variable types. So if you want a generic function that works against any integer, you need to figure out all the traits you need. When in reality, I would like to say that type T is “int-alike”.

Personal hate against f32 and f64 traits

This might be only me, but every time I add a float in Rust makes my life hard. The fact that it doesn’t support proper ordering and proper equality makes them unusable on lots of collection types (HashMaps, etc).

Yes, I know that these types don’t handle equality (due to imprecision) and comparing them is also tricky (due to NaN and friends). But, c’mon… can’t we have a “simple float”?

On some cases, like configs, decimal numbers are convenient. I wouldn’t mind using a type that is slower for those cases, that more or less handles equality (by having an epsilon inbuilt) and handles comparison (by having a strict ordering between NaN and Inf, or by disallowing it at all).

This is something that causes pain to me every time I use floats.

Why I think Rust will not replace Python

Take into account that I’m still learning Rust, I might have missed or be wrong on some stuff above. One year of practising on my own is not enough to have enough context for all of this, so take this article with a pinch of salt.

Rust is way too different to Python. I really would like Rust to replace my use on Python but seeing there are some irreconcilable differences makes me believe that this will never happen.

WASM might be able to bridge some gaps, and Diesel and other ORM might make Rust a better replacement of Python for REST APIs in the future.

On the general terms I don’t see a lot of people migrating from Python to Rust. The learning curve is too steep and for most of those replacements Go might be enough, and therefore people would skip Rust altogether. And this is sad, because Rust has a lot of potentials on lots of fronts, just requires more attention than it has.

I’m sad and angry because this isn’t the article I wanted to write. I would like to say that Rust will replace Python at some point, but if I’m realistic, that’s not going to happen. Ever.

References

https://blog.logrocket.com/rust-vs-python-why-rust-could-replace-python/

https://www.reddit.com/r/functionalprogramming/comments/kwgiof/why_do_you_think_data_scientists_prefer_python_to/glzce8e/?utm_source=share&utm_medium=web2x&context=3

Benchmarking Python vs PyPy vs Go vs Rust

Since I learned Go I started wondering how well it performs compared to Python in a HTTP REST service. There are lots and lots of benchmarks already out there, but the main problem on those benchmarks is that they’re too synthetic; mostly a simple query and far from real world scenarios.

Some frameworks like Japronto exploit this by making the connection and the plain response blazing fast, but of course, as soon as you have to do some calculation (and you have to, if not what’s the point on having a server?) they fall apart pretty easily.

To put a baseline here, Python is 50 times slower than C++ on most benchmarks, while Go is 2-3 times slower than C++ on those and Rust some times even beats C++.

But those benchmarks are pure CPU and memory bound for some particular problems. Also, the people who submitted the code did a lot of tricks and optimizations that will not happen on the code that we use to write, because safety and readability is more important.

Other type of common benchmarks are the HTTP framework benchmarks. In those, we can get a feel of which languages outperform to others, but it’s hard to measure. For example in JSON serialization Rust and C++ dominate the leader board, with Go being only 4.4% slower and Python 10.6% slower.

In multiple queries benchmark, we can appreciate that the tricks used by the frameworks to “appear fast” no longer are useful. Rust is on top here, C++ is 41% slower, and Go is 43.7% slower. Python is 66.6% slower. Some filtering can be done to put all of them in the same conditions.

While in that last test which looks more realistic, is interesting to see that Python is 80% slower, which means 5x from Rust. That’s really really far better from the 50x on most CPU benchmarks that I pointed out first. Go on the other hand does not have any benchmark including any ORM, so it’s difficult to compare the speed.

The question I’m trying to answer here is: Should we drop Python for back-end HTTP REST servers? Is Go or Rust a solid alternative?

The reasoning is, a REST API usually does not contain complicated logic or big programs. They just reply to more or less simple queries with some logic. And then, this program can be written virtually with anything. With the container trend, it is even more appealing to deploy built binaries, as we no longer need to compile for the target machine in most cases.

Benchmark Setup

I want to try out a crafted example of something slightly more complicated, but for now I didn’t find the time to craft a proper thing. For now I have to fall back into the category of “too synthetic benchmarks” and release my findings up to this point.

The base is to implement the fastest possible for the following tests:

  • HTTP “Welcome!\n” test: Just the raw minimum to get the actual overhead of parsing and creating HTTP messages.
  • Parse Message Pack: Grab 1000 pre-encoded strings, and decode them into an array of dicts or structs. Return just the number of strings decoded. Aims to get the speed of a library decoding cache data previously serialized into Redis.
  • Encode JSON: Having cached the previous step, now encode everything as a single JSON. Return the number characters in the final string. Most REST interfaces will have to output JSON, I wanted to get a grasp how fast is this compared to other steps.
  • Transfer Data: Having cached the previous step, now send this data over HTTP (133622 bytes). Sometimes our REST API has to send big chunks over the wire and it contributes to the total time spent.
  • One million loop load: A simple loop over one million doing two simple math operations with an IF condition that returns just a number. Interpreted languages like Python can have huge impact here, if our REST endpoint has to do some work like ORM do, it can be impacted by this.

The data being parsed and encoded looks like this:

{"id":0,"name":"My name","description":"Some words on here so it looks full","type":"U","count":33,"created_at":1569882498.9117897}

The test has been performed on my old i7-920 capped at 2.53GHz. It’s not really rigorous, because I had to have some applications open while testing so assume a margin of error of 10%. The programs were done by minimal effort possible in each language selecting the libraries that seemed the fastest by looking into several benchmarks published.

Python and PyPy were run under uwsgi, sometimes behind NGINX, sometimes with the HTTP server included in uwsgi; whichever was faster for the test. (If anyone knows how to test them with less overhead, let me know)

The measures have been taken with wrk:

$ ./wrk -c 256 -d 15s -t 3 http://localhost:8080/transfer-data

For Python and PyPy the number of connections had to be lowered to 64 in order to perform the tests without error.

For Go and Rust, the webserver in the executables was used directly without NGINX or similar. FastCGI was considered, but seems it’s slower than raw HTTP.

Python and PyPy were using Werkzeug directly with no url routing. I used the built-in json library and msgpack from pip. For PyPy msgpack turned out to be awfully slow so I switched to msgpack_pypy.

Go was using “github.com/buaazp/fasthttprouter” and “github.com/valyala/fasthttp” for serving HTTP with url routing. For JSON I used “encoding/json” and for MessagePack I used “github.com/tinylib/msgp/msgp”.

For Rust I went with “actix-web” for the HTTP server with url routing, “serde_json” for JSON and “rmp-serde” for MessagePack.

Benchmark Results

As expected, Rust won this test; but surprisingly not in all tests and with not much difference on others. Because of the big difference on the numbers, the only way of making them properly readable is with a logarithmic scale; So be careful when reading the following graph, each major tick means double performance:

Here are the actual results in table format: (req/s)


HTTPparse mspencode jsontransfer data1Mill load
Rust128747.615485.435637.2019551.831509.84
Go116672.124257.063144.3122738.92852.26
PyPy26507.691088.88864.485502.14791.68
Python21095.921313.93788.767041.1620.94

Also, for the Transfer Data test, it can be translated into MiB/s:


transfer speed
Rust2,491.53 MiB/s
Go2,897.66 MiB/s
PyPy701.15 MiB/s
Python897.27 MiB/s

And, for the sake of completeness, requests/s can be translated into mean microseconds per request:


HTTPtransfer dataparse mspencode json1Mill load
Rust7.7751.15182.30177.39662.32
Go8.5743.98234.90318.031,173.35
PyPy37.72181.75918.371,156.761,263.14
Python47.40142.02761.081,267.8147,755.49

As per memory footprint: (encoding json)

  • Rust: 41MB
  • Go: 132MB
  • PyPy: 85MB * 8proc = 680MB
  • Python: 20MB * 8proc = 160MB

Some tests impose more load than others. In fact, the HTTP only test is very challenging to measure as any slight change in measurement reflects a complete different result.

The most interesting result here is Python under the tight loop; for those who have expertise in this language it shouldn’t be surprising. Pure Python code is 50x times slower than raw performance.

PyPy on the other hand managed under the same test to get really close to Go, which proves that PyPy JIT compiler actually can detect certain operations and optimize them close to C speeds.

As for the libraries, we can see that PyPy and Python perform roughly the same, with way less difference to the Go counterparts. This difference is caused by the fact that Python objects have certain cost to read and write, and Python cannot optimize the type in advance. In Go and Rust I “cheated” a bit by using raw structs instead of dynamically creating the objects, so they got a huge advantage by knowing in advance the data that they will receive. This implies that if they receive a JSON with less data than expected they will crash while Python will be just fine.

Transferring data is quite fast in Python, and given that most API will not return huge amounts of it, this is not a concern. Strangely, Go outperformed Rust here by a slight margin. Seems that Actix does an extra copy of the data and a check to ensure UTF-8 compatibility. A low-level HTTP server probably will be slightly faster. Anyway, even the slowest 700MiB/s should be fine for any API.

On HTTP connection test, even if Rust is really fast here, Python only takes 50 microseconds. For any REST API this should be more than enough and I don’t think it contributes at all.

On average, I would say that Rust is 2x faster than Go, and Go is 4x faster than PyPy. Python is from 4x to 50x slower than Go depending on the task at hand.

What is more important on REST API is the library selection, followed by raw CPU performance. To get better results I will try to do another benchmark with an ORM, because those will add a certain amount of CPU cycles into the equation.

A word on Rust

Before going all the way into developing everything in Rust because is the fastest, be warned: It’s not that easy. Of all four languages tested here, Rust was by far, the most complex and it took several hours for me, untrained, to get it working at the proper speed.

I had to fight for a while with lifetimes and borrowing values; I was lucky to have the Go test for the same, so I could see clearly that something was wrong. If I didn’t had these I would had finished earlier and call it a day, leaving code that copies data much more times than needed, being slower than regular Go programs.

Rust has more opportunities and information to optimize than C++, so their binaries can be faster and it’s even prepared to run on crazier environments like embedded, malloc-less systems. But it comes with a price to pay.

It requires several weeks of training to get some proficiency on it. You need also to benchmark properly different parts to make sure the compiler is optimizing as you expect. And there is almost no one in the market with Rust knowledge, hiring people for Rust might cost a lot.

Also, build times are slow, and in these test I had always to compile with “–release”; if not the timings were horribly bad, sometimes slower than Python itself. Release builds are even slower. It has a nice incremental build that cuts down this time a lot, but changing just one file requires 15 seconds of build time.

Its speed it’s not that far away from Go to justify all this complexity, so I don’t think it’s a good idea for REST. If someone is targeting near one million requests per second, cutting the CPU by half might make sense economically; but that’s about it.

Update on Rust (January 18 2020): This benchmark used actix-web as webserver and it has been a huge roast recently about their use on “unsafe” Rust. I’m had more benchmarks prepared to come with this webserver, but now I’ll redo them with another web server. Don’t use actix.

About PyPy

I have been pleased to see that PyPy JIT works so well for Pure Python, but it’s not an easy migration from Python.

I spent way more time than I wanted on making PyPy work properly for Python3 code under uWSGI. Also I found the problem with MsgPack being slow on it. Not all Python libraries perform well in PyPy, and some of them do not work.

PyPy also has a high load time, followed by a warm-up. The code needs to be running a few times for PyPy to detect the parts that require optimization.

I am also worried that complex Python code cannot be optimized at all. The loop that was optimized was really straightforward. Under a complex library like SQLAlchemy the benefit could be slim.

If you have a big codebase in Python and you’re wiling to spend several hours to give PyPy a try, it could be a good improvement.

But, if you’re thinking on starting a new project in PyPy for performance I would suggest looking into a different language.

Conclusion: Go with Go

I managed to craft the Go tests in no time with almost no experience with Go, as I learned it several weeks ago and I only did another program. It takes few hours to learn it, so even if a particular team does not know it, it’s fairly easy to get them trained.

Go is a language easy to develop with and really productive. Not as much as Python is, but it gets close. Also, it’s quick build times and the fact that builds statically, makes very easy to do iterations of code-test-code, being attractive as well for deployments.

With Go, you could even deploy source code if you want and make the server rebuild it each time that changes if this makes your life easier, or uses less bandwidth thanks to tools like rsync or git that only transfer changes.

What’s the point of using faster languages? Servers, virtual private servers, server-less or whatever technology incurs a yearly cost of operation. And this cost will have to scale linearly (in the best case scenario) with user visits. Using a programming language, frameworks and libraries that use as less cycles and as less memory as possible makes this year cost low, and allows your site to accept way more visits at the same price.

Go with Go. It’s simple and fast.

Why Java is faster than Python

And why C is faster than Java.

On some of the things you’ll hear, there is the classic “there is no faster or slower languages, it depends on what purpose you want to use it, some languages are better fit than others”. While part is true (some languages are better fit for some tasks than others), the other part is false. There are faster and slower languages. That is a fact.

The other thing I heard is “Java is a compiled language and Python is interpreted, therefore, Java is much faster”. Also false. Java is interpreted as Python, or Python compiled as Java. Both languages compile (or transpile) to bytecode, and a interpreter then executes those instructions.

But Java has JIT! Well, and PyPy also does have JIT. So what?

And Java is also not older than Python, they’re both same age more or less.

Regardless of any of those typical comments and counter-arguments, the fact is that Java is 4x slower than C and Python is 40x slower than Java (more or less, depends greatly on the benchmark). (And the old C does not have JIT, hah!)

So, why Java is faster than Python?

It’s just because Java leverages way more work and responsibility on the developer than Python. This is just a trade-off between computer performance and developer performance. How is this possible?

First, we have to understand what compilers do and how optimizations work. The compiler serves basically one fundamental purpose and it’s not generating an executable or bytecode. The purpose is to solve as much work as possible beforehand. As a side-effect, it has to write an executable or bytecode that could be read later to follow the instructions. Just to be clear, the compiler job is to remove complexity and uncertainty from the source code and write an output that is as dumb-stupid as possible so it can be followed blindly. The more stuff you remove, the faster it gets later.

Which kind of stuff we can remove or simplify for later? Well, the first step is the parsing stage, parsing a source file takes a lot of time; so the most basic compiler would read the code and output a binary abstract syntax tree that can be loaded onto memory really quick. Then, would be doing simple math stuff ahead of time. Also removing dead code.

But from here it gets tricky. For the CPU handling the instructions we need to know what we’re doing ahead of time, which data types, sizes, expected result types and so. Depending on the language this might be possible but it is not the case for Python. It has way too much abstraction and craziness inbuilt that we could never know what to expect at a particular part of the program. Every variable, even if it looks simple, can be replaced by a different thing by mocking or other kind of weird techniques in Python. So the only way around is to wrap the Python values in a complex structure that can track all those changes. In doing so, we lose all running performance in favour of being a friendlier (and crazier) language.

Java has static typing and this helps a lot translating all instructions into real CPU instructions. But for that step to be done we need a Just In Time compilation (JIT); if not, we would still feed the instructions one by one using the interpreter.

But C is faster. Its trick is moving more burden from the language to the developer. In this case, we not only require the developer to type everything and define every behaviour ahead of time; we also require the developer to have responsibility on the memory access and on the program behaviour. So in C, if a program closes unexpectedly is never C fault, it’s always the developer responsibility to check everything.

This in C is called “Undefined Behaviour” and describes those grey areas where the C compiler just “it doesn’t care”, it will assume everything is good and optimize as much as possible. The C compiler can even replace function calls with the expected result on the final executable file. It can also decide to “unroll” a function into the caller because it believes that is the same result, and faster.

So in short, the flexibility of a language and its “auto-magic” has huge trade-offs in performance. Writing a Java interpreter or compiler that is faster than Python should be easy. But writing a Python interpreter that could be fast enough to be compared to Java is almost impossible.

I want to add a special mention to JavaScript, probably the most hated language lately. Being a bit less auto-magic than Python and having huge efforts to implement faster JS engines in browsers has led JavaScript to have JIT and lots of sorts of optimizations, leaving us with one of the fastest interpreted languages that exists up to now:

https://benchmarksgame-team.pages.debian.net/benchmarksgame/faster/javascript.html

JavaScript lacks thread support, because browsers don’t want scripts to mess with them for security and stability reasons. Still, if you account that most of those benchmarks I shared earlier Java was using all CPUs and JavaScript only one, it seems that JavaScript performance is somewhat close to Java, which is impressive.

Still I would not recommend (yet) JavaScript for server-side applications. But nonetheless it is an interesting outcome of being the only standard scripting language over the web.

Infraestructura de desarrollo web con python

Feliz año nuevo! Empezamos el 2019 y he querido rescatar un artículo que tenía pendiente hace bastante. Cada vez más veo más gente pensando en hacer desarrollo web con Python en vez de con PHP.

¿Cual es la ventaja de Python sobre PHP? PHP nació anticuado y por mucho que intentan mejorarlo, las bases sobre las que se fomentan son arenas movedizas como en Javascript. Python es mucho más robusto y más seguro que PHP.

Pero las cosas claras, en cuanto a velocidad del lenguaje per-sé, Python es varias veces más lento que PHP. Y si ya lo era antes, ahora que PHP en su versión 7 ha mejorado la velocidad muchísimo, ciertamente Python se queda muy atrás en este tema.

Aunque ya he dicho un montón de veces, Python es rápido al final por el ecosistema en sí, si se sabe usar correctamente. Todos los llenguajes son lentos si se usan mal; por mucho que trabajes en C++, he visto programas en Python hacer lo mismo en menos tiempo, únicamente porque estaban mejor hechos. Y éste lenguaje facilita mucho hacer las cosas bien.

PHP por otro lado tiende a fallar, perder memoria por el camino y la plataforma que tiene para funcionar vía web hace que tenga un coste significativo sólo lanzar el programa. Como programador web os puede gustar (a gustos colores), pero como Dev-Ops o Administrador de Sistemas sólo puedes odiarlo. Es bastante coñazo de mantener un sistema estable en un servidor web con varias páginas complejas.

Si en este año nuevo os estáis planteando probar Python para web, os comento por donde empezar:

uWSGI

Python se conecta al servidor web (Apache, Nginx, etc) de muchas formas. Por defecto la mayoría de frameworks levantan un mini servidor web que podéis conectar vía Proxy HTTP. Pero esto sólo es recomendable para sistemas de desarrollo en local.

En servidores tenemos FastCGI y uWSGI. El segundo es más nuevo, fácil de configurar y más rápido. Por ejemplo en Apache tenéis mod_uwsgi y es sencillo:

ProxyPass /foo uwsgi://127.0.0.1:3032/

Además Python no es como PHP en cuanto a la ejecución de los ficheros. Los programas Python no se dejan en la misma carpeta donde están las imágenes. En nuestro caso hay una clara separación entre lo que son programas y datos. Si vienes de PHP puede parecer un engorro comparado con dejar el fichero x.php donde te plazca, pero en cuanto a seguridad es muchísimo mejor; no hay ninguna posibilidad de que el código fuente sea visible desde la web por accidente y ficheros subidos a la web no se pueden ejecutar nunca.

Comparad esto con PHP, donde a veces basta con subir un adjunto con extensión “.php” y luego ir a la URL.

Flask y Django

Si váis a empezar con Python lo mejor es que uséis un Framework donde ya venga todo. Si os gustan los frameworks y/o empezar desde una base que te guíe, Django es ideal. Si prefieres algo más parecido al PHP en crudo, Flask es muy minimalista y te deja trabajar a tu gusto. Se puede bajar aún más i ir a Werkzeug, pero habiendo probado los tres creo que Flask es la mejor recomendación.

Django está bien si quieres una estructura y poder instalar módulos de terceros que hay un montón. Pero si eres organizado y puedes crear cosas rápido, Flask es flexible y más rápido que Django.

Además estos proyectos tienen todos documentación de cómo conectarlos con servidores web correctamente, cuales son los estándares de seguridad a seguir, etcétera.

PostgreSQL

Para base de datos, he trabajado un montón con MySQL, otro tanto con PostgreSQL y también con SQLServer.

SQLServer no lo recomiendo para nada. MySQL es la solución típica para webs, pero lo único realmente bueno que tiene es una caché integrada en la base de datos. PostgreSQL supera a ambas y no por poco. Es excelente, llena de funcionalidades y robusta a más no poder. La única pega para web es que no tiene una caché integrada, por lo que si lanzas la misma consulta 40 veces, se ejecutará 40 veces. Lo que hay que hacer es cachear nosotros lo que nos interese para evitar esto

Las bases de datos NoSQL no hace falta ni que las miréis. Las pocas ventajas que puedan tener sobre una SQL, o son funcionalidades que puedes tener en PostgreSQL, o no le sacas partido porque no tienes suficiente cantidad de datos para que valga la pena. En mis pruebas PostgreSQL era más del doble de rápido que MongoDB (la más rápida) bajo las mismas condiciones. Y luego las NoSQL no son ni consistentes ni confiables, parte de su rapidez viene en que pueden perder datos.

Sqlalchemy

A la hora de acceder a las bases de datos, los ORM son muy recomendables porque su abstracción permite que un mismo código pueda trabajar con los datos sin saber de dónde vienen. Además simplifican mucho el desarrollo y mejoran la lectura.

La pega de los ORM (de todos) es que agregan una carga extra al procesar los datos. Después de revisar y probar SqlAlchemy me pareció que daba un buen resultado. Comparado con el ORM de Django, tiene más funcionalidades y es más rápido.

Hay otros (menos funcionales), pero como no los he podido probar no sé decir si son más rápidos.

Nginx

Evitad usar Apache, y si lo usáis al menos que no sea en Prefork. Nginx es mi favorito como Sysadmin ya que no da dolores de cabeza, funciona, tiene lo que necesito y es extremadamente rápido. Los servidores con Nginx siempre me han funcionado extremadamente bien.

Redis

Que queréis un servidor NoSQL, pues tenéis Redis. Que necesitáis una caché, pues Redis. Redis es ideal para datos temporales, de alta frecuencia de acceso y escritura. La pega es que en seguridad va corto, como todos los servidores de este tipo. Así que si lo usáis, procurad que sólo tengan acceso los programas que deben tenerlo.

Redis se puede configurar con casi todos los frameworks, lenguajes de programación o incluso muchos CMS. Y además soporta clústering y sharding! ¿Qué más podéis pedir?

Ansible

En el último año he trabajado con Ansible, que es una herramienta para orquestrar servidores. Aunque es un poco costoso de empezar con ella, lo bueno es que da resultados bastante reproducibles, con la facilidad de que lo que va en una máquina tiende a ir en las demás. Y las instrucciones de instalación se os quedan guardadas en Git, por lo que instalar los siguientes servidores es más fácil.

Además que, el poder desplegar una actualización a todos los equipos en un sólo comando es una pasada. Cuando tienes 50 equipos que necesitan ser actualizados, o instalar un nuevo programa, Ansible consigue que parezca trivial.

Docker

Pero si queremos ir a la reproducibilidad máxima, a poder trabajar exactamente igual en local que en el servidor, Docker es aún mejor que Ansible. Aunque ambos se pueden usar a la vez y complementarse.

Docker además permite tener distintas aplicaciones aisladas entre sí de forma que una aplicación comprometida no pone en riesgo el servidor o las otras aplicaciones.

En las próximas semanas, cada jueves, iré publicando una serie de artículos sobre cómo usar Docker para hacer un servidor LAMP ultra-seguro. Estad atentos!


Python threading can use all cores despite the GIL

We can read in most places that Python cannot use all cores and its threads are useless because of something called GIL, that prevents two python instructions being executed at the same time.

While this is true, as in much things in Python, in practice is a bit different. Python language is well thought and very clever in ways that most people would not expect at first.

What is this infamous GIL? GIL stands for Global Interpreter Lock and it is a thread lock that prevents two threads from executing any instruction in Python at the same time. Its reasoning is, Python cannot guarantee any safety while writting and reading the same variables at the same time, and the garbage collector (GC) also counts on that to work properly.

But Python does not only execute Python instructions, it also executes C functions, and of course there is the I/O operations. This is one common thing that people misses when reasoning on Python performance. Python is not like Java; it does not execute Python under a python virtual machine or similar, it is an actual interpreter (which makes it way slower) which is written in C and it also interfaces with C, C++ and Fortran.

This might seem as a disadvantage, but as it is C code running, as soon as there’s no Python variables involved anymore (because they were copied into C memory space) the GIL can be released. Yes! The GIL indeed can be obtained and released from C code. And as soon as this happens other Python code can run in parallel.

It turns out that a large amount of community Python libraries and built-in ones have parts or all written in C; mainly those parts that are computationally expensive.

So Python in practice tends to be way faster than expected in practice, and paralellism is actually better than predicted by theory.

If you’re using Python, you should be aware that it is more like a director when it handles the complex logic and controls other libraries to do the actual stuff. Don’t try to do yourself a fancy-fast algorithm that looks complex; most cases like this in Python end by being slower, because you’re doing it in Python instead of C.

It is threading useless in Python? Definitely not. Networking, I/O or paralelizing other libraries are useful examples on how to use them properly. For I/O there are also asynchronous functions that could do more or less the same as with threads if the operating system is supported and the particular I/O task is supported.

Can Python use all cores? No, in the conventional way, but if we’re smart and send the tasks out, definitely yes. Also check Cython and Numba for creating your own solutions that interface with C and how to release the GIL.

And finally, if we go interfacing with C, our program is going to be faster than any Java or Scala program, because, well, it is C.

Diseñando una web ultra-rápida (V)

En el anterior post vimos cómo instalar y configurar nginx con caché y cómo comprobar el rendimiento. En este post me voy a centrar en cómo no servir contenido obsoleto.

Refrescando la caché

Lógicamente si la caché dura un día, los usuarios tardarán un día desde que envían un mensaje hasta que se pueda ver. Incluso ellos mismos no verían su mensaje, lo que es muy confuso.

Sin caché, estaríamos hablando entre 1000 y 2000 por segundo, que también es muy rápido. Como el backend sólo realiza una tarea muy básica, le permite entregar una cantidad de peticiones muy alta. Gracias a esto, podemos permitirnos que las cachés sean más pequeñas si queremos, ya que el tiempo de generar el contenido es realmente bajo.

Podemos establecer unos tiempos de caché distintos según el tipo de contenido que sea. Por ejemplo para los foros puede ser bastante largo. Para los hilos un tiempo intermedio y para los mensajes sería más corto.

Es más, como el backend puede devolver para cada petición unas cabeceras de control de caché distintas, podemos hacer que los hilos viejos y los mensajes viejos que tienen menos probabilidad de cambiar, que tengan una caché más grande. Especialmente las páginas de mensajes que tienen más de un par de días. Si tenemos bloqueadas las ediciones pasado un tiempo, ya tenemos garantizado que éstos no cambian.

El contenido que puede cambiar, la caché como mucho va a ser de escasos segundos. Y a veces incluso eso va a ser mucho en algunos casos ya que la aplicación podría pedir el post que se acaba de escribir y no lo vería.

Para solucionar eso lo ideal es que al realizar la petición PUT/POST desde javascript, el servidor devuelva el JSON entrada con él y lo integre. De ese modo una caché de unos pocos segundos no le afecta y ve el contenido instantáneamente. Los demás tardarán un poco, por ejemplo 5 segundos.

Pero esto quiere decir que si un hilo recibe mucha actividad, vamos a tener muchos usuarios leyendo la última página y refrescando. Cada poco, vamos a tener que generar una versión nueva. La ventaja es que la caché de 5 segundos va a enviar la misma copia a varias personas, por lo que la carga ya se va a reducir muchísimo.

Pero esto se puede mejorar aún más.

304 Revalidate con NGINX

En NGINX se puede habilitar una opción de caché llamada “revalidate“, que cuando la caché caduca, envía al servidor una petición con E-Tag y/o If-Modified-Since, y el servidor puede responder con HTTP 304 Not Modified. Si esto sucede, NGINX renueva la caché. Ésta respuesta “Not Modified” deberá tener también las cabeceras Cache-Control y en ellas podemos incluso definir nuevos parámetros de persistencia si queremos.

Supongo que sabéis cómo funciona: En cada petición en el backend generamos o una fecha de modificación, o un string (tipo una firma) llamado E-Tag. Cuando el cliente web nos envía una petición para la que ya tiene caché, incluye éstos parámetros y podemos comparar si su versión es aún válida o la hemos cambiado. Si ha cambiado o no provee éstas cabeceras, realizamos la petición normal y devolvemos HTTP 200. Si no ha cambiado, devolvemos HTTP 304, sin contenido.

Para hacer esto en Nginx es tan sencillo como activar cache_revalidate:

uwsgi_cache_revalidate on;

La ventaja de esto es que no tenemos que generar el contenido otra vez y no tenemos que enviarlo. El truco reside en encontrar la forma de saber si ha cambiado sin apenas usar recursos. Si en la base de datos tenemos una fecha de modificación, podemos usar esa. También se puede usar redis u otro sistema de caché en memoria para almacenar la versión y actualizarla cada vez que envíen contenido nuevo.

También está cache_lock que es interesante. Si se activa, cuando llegan dos peticiones para la misma página y no tiene caché, sólo lanza una petición al backend y la segunda petición se espera. Muy útil para evitar consumo de recursos extra a costa de que el cliente espere un poco. De todos modos es muy posible que la primera petición termine antes que la segunda, por lo que no debería haber ningún retraso percibido, con la ventaja de consumir menos:

uwsgi_cache_lock on;

Volvamos a revisar la gráfica que hice al principio:

grafica

En el extremo izquierdo hay 3000 peticiones por segundo para las consultas que no pesan casi nada, y en el derecho unas 300 por segundo para las que ya tienen un tamaño considerable. Esto quiere decir que para contenido grande, podemos hacer que NGINX lo mantenga en caché a un ritmo 10 veces más rápido que generarlo de nuevo! Es como una caché sin caché. O caché inteligente.

Incluso podría ser mejor. Puede que haya contenido que requiera mucha lógica para computarlo aunque no pese demasiado. Este contenido se beneficiaría aún más de ésta práctica.

Con éste sistema, la caché se puede bajar hasta 1 segundo y el ritmo de entrega sigue siendo muy bueno. Aún así, hay que seguir considerando el cachear más tiempo el contenido que no va a cambiar, porque aún es tres veces más rápido que el 304.

Hago la prueba con caché de 1 segundo, gestión del 304 y a medir con siege:

$ siege -f urls3.txt -c 8 -ib -t 60s
(...)
Lifting the server siege...
Transactions: 216952 hits
Availability: 100.00 %
Elapsed time: 59.61 secs
Data transferred: 242.55 MB
Response time: 0.00 secs
Transaction rate: 3639.52 trans/sec
Throughput: 4.07 MB/sec
Concurrency: 7.46
Successful transactions: 216952
Failed transactions: 0
Longest transaction: 0.27
Shortest transaction: 0.00

Como vemos, llego a las 3639 peticiones por segundo. Es decir, a pesar de servir un contenido que me daba 2500, se sirve a una velocidad altísima. Y como comentaba, si el tamaño de la respuesta aumenta, ésta velocidad no se reduce apenas porque estamos devolviendo sólo un 304. El truco es devolver el 304 rápidamente, en mi caso siempre lo hago sin ningún cálculo por lo que es de esperar que en una web real haya que calcular un poco.

Métodos de refresco forzado

Aún se puede mejorar más, pues NGINX tiene otras opciones de interés. Una es el stale-while-revalidate, que hace que entregue a los clientes una copia antigua enseguida mientras él la actualiza por debajo.

 proxy_cache_use_stale updating error timeout http_500 
       http_502 http_503 http_504;
 proxy_cache_background_update on;

Al hacer estos cambios y probar, ya veo un aumento en el rendimiento:

Transactions: 239757 hits
Availability: 100.00 %
Elapsed time: 59.72 secs
Data transferred: 267.79 MB
Response time: 0.00 secs
Transaction rate: 4014.69 trans/sec
Throughput: 4.48 MB/sec
Concurrency: 7.43
Successful transactions: 239758
Failed transactions: 0
Longest transaction: 0.23
Shortest transaction: 0.00

El problema de esto es que no podemos especificar cuan vieja puede llegar a ser la caché, por lo que aunque nuestra caché sea de 1 segundo, si durante una hora nadie la pide, el siguiente que la pida verá la de hace una hora y se actualizará en segundo plano.

Si queremos corregir esto, en lugar de usar estos comandos hay que usar las cabeceras http Cache-Control con stale-while-revalidate y stale-if-error, que permiten especificar el máximo de tiempo. Pero no estoy seguro de si Nginx sigue los tiempos también.

Otra de las opciones es purgar la caché a demanda. Se puede hacer que al recibir ciertas peticiones nginx vacíe la caché. Primero establecemos que cuando se reciba una petición tipo PURGE nos asigne una variable a 1:

    map $request_method $purge_method {
        PURGE 1;
        default 0;
    }

Luego asignamos este $purge_method como flag al comando cache_purge:

uwsgi_cache_purge $purge_method;

Esto resulta en que cuando se recibe una petición de tipo PURGE, la url especificada se elimina de la caché. También acepta patrones, por lo que se puede hacer:

$ curl -X PURGE -D – "https://www.example.com/api/forums/1/*"

Otra opción son URLs que no leen de caché, pero sí guardan. Lo normal es que si una URL guarda a la caché, también lea de la caché. Y que si no lee, tampoco guarde. Esto es así porque si una URL sirve contenido basado en cookies, podría estar cacheando contenido de un usuario autenticado, con información privada y luego servirla a anónimos.

Pero si hablamos de urls que siempre devuelven lo mismo, se puede pensar en una URL alternativa o una cabecera que hace que guarde en caché el contenido nuevo, sin leerlo nunca de la caché. Y sería muy útil para en lugar de purgar la caché, refrescarla con antelación cuando sabemos que ha cambiado.

En nginx esta función la realiza “cache_bypass” y se asigna a las variables que queramos. Si cualquiera de ellas devuelve algo distinto de vacío o cero, el contenido devuelto es nuevo. Si no se especifica “no_cache” la respuesta obtenida se guardará igualmente en la caché.

proxy_cache_bypass $bypass_request

Luego ya es lanzar un wget –mirror o similar con el método adecuado o cabeceras adecuadas, y nginx refrescará la caché.

Aunque igual es más sencillo purgarla por patrón y luego llenarla de nuevo consultando normalmente. Eso ya es cuestión de gustos y de las ventajas/inconvenientes para lo que queramos hacer exactamente.

Lo dejamos aquí por hoy. En el próximo post volvemos a la carga acelerando las páginas que requieren autenticación.

Nos vemos pronto!

Diseñando una web ultra-rápida (IV)

Después de haber instalado y configurado NGINX con uWSGI, en el post de hoy toca replantearse cosas y empezar con la caché.

(Re)diseño de la API

Después del test que realizamos en la entrada anterior me doy cuenta que mi diseño actual de la API está mal orientado. Para probar https/2 hice una API con muchas urls muy pequeñas para demostrar la cantidad de peticiones que el navegador puede consultar con HTTP2. (unas 100 por segundo, a través de internet)

Mi esquema de URLs (es un foro) era el siguiente:

  • /user/<user_id> ; Obtiene el perfil de un usuario por ID
  • /category/ ; Obtiene un listado inicial de categorías de foros
  • /category/<category_id>/forum ; Obtiene una lista de foros dentro de una categoría, la primera página
  • /category/<int:category_id>/forum/page/<int:page_id> ; lo anterior, para las siguientes páginas. (5 por página)
  • /category/<int:category_id>/forum/<int:forum_id>/topic/page/<int:page_id>
    obtiene una lista de hilos, por páginas. 10 por página.
  • /category/<int:category_id>/forum/<int:forum_id>/topic/<int:topic_id>/post/<int:post_id>
    obtiene un mensaje en particular

El resultado es que la mayoría de los mensajes están entre 0.5Kb y 1Kb. Siguiendo la gráfica que vimos está claro que esto tiene un rendimiento muy inferior a lo que podría ser. Os la pongo de nuevo:

grafica

Hay que empaquetar más las llamadas, ya que el mejor rango de trabajo está entre los 4kB y los 16kB. Por desgracia algunas no se pueden, porque los usuarios no tiene sentido empaquetarlos en páginas. Las categorías sí se pueden, ya que se podría exportar prácticamente todo el listado de foros junto a las categorías. Los hilos se pueden hacer en 100 por página y los mensajes también se pueden paginar según los hilos de igual forma.

Esto hay que plantearlo correctamente, porque también es tirar mucho ancho de banda a la basura si terminamos sirviendo muchos datos que el cliente al final no necesita.

El principal problema es que, como queremos servirlo como estático, no queremos parámetros de consulta, por lo que el cliente no puede pasarnos un listado de los elementos que quiere. Para eso tiene que realizar consultas HTTP separadas, que al ser HTTP2 debería ser rápido.

El nuevo diseño podría ser como sigue:

  • /user/<user_id> ; Obtiene el perfil de un usuario por ID
  • /forum/ ; obtiene el listado de todas las categorías y todos sus foros. Incluye el número de hilos dentro de cada uno o el número de página final.
  • /forum/<int:forum_id>/page/<int:page_id> ; obtiene los hilos de un foro, a 200 por página. Incluye el foro otra vez, el número de mensajes/página final y también incluye nombres y url de avatares de los usuarios que aparecen.
  • /forum/<int:forum_id>/topic/<int:page_id>page/<int:page_id> ; obtiene los mensajes de hilo, a 50 por página. Incluye el topic otra vez y los nombres de usuario y sus avatares.

Os preguntaréis porqué me empeño en hacer las url jerárquicas, ya que un topic_id identifica un foro y no se repite en distintos foros. El motivo es que facilita después restringir el acceso por URL. Si no tienes acceso a un foro, se puede decir que te deniego el acceso a /forum/2/*, y no tengo que aplicar más reglas. Para eso, las urls jerárquicas tienen que comprobar que efectivamente estamos pidiendo el foro (o el hilo) que le hemos solicitado, pero se puede cachear en memoria fácilmente.

En cuanto al diseño de base de datos, paginar es bastante caro. Cuando pides a MySQL o PostgreSQL (o cualquier otra) que te devuelva la página 5 ordenando por id o fecha, no tiene más remedio que leer todos los mensajes hasta llegar a la página 5. Por esto, esta estrategia es poco eficiente con hilos de muchas páginas. Es recomendable agregar una columna de página o de nº de mensaje para poder filtrar con un Where. Otra opción es usar otra tabla que mantenga una relación de qué id es el primero de cada página.

Como MySQL cachea las consultas, esto no parece tener mucho efecto, pero en realidad MySQL no sabe muy bien cuando una modificación en una tabla debería purgar una caché, así que si alguien envía cualquier mensaje a cualquier foro, MySQL eliminará de la caché todas las consultas que lean mensajes. Agregar nosotros esta caché manualmente en forma de tabla o en REDIS nos permite purgarla o regenerarla cuando creamos conveniente. Tiene más trabajo, pero sacas más rendimiento.

Con el nuevo esquema, deberían haber muchísimas menos peticiones desde la web, éstas contendrán mucha más información, que a su vez es útil para el renderizado de la página que solicita el usuario.

Aún estoy pendiente de realizar estos cambios y van a ser un montón en el frontend Angular 6 que tengo. Así que no puedo sacar estadísticas de rendimiento aún. Esto tendrá que esperar más adelante.

De momento vamos a ver cómo funciona la API antigua con caché.

Configurando la caché en NGINX

Dependiendo de si habéis usado uwsgi, proxy o fastcgi, los comandos de caché tienen unos nombres ligeramente distintos, empiezan con el nombre del módulo y luego el comando. Pero la sintaxis es la misma. Yo lo hice con uwsgi, pero cambiar a otro es sólo reemplazar este nombre por el nombre de vuestro módulo.

Quedaría así:

uwsgi_cache_path /tmp/nginx-static levels=1:2 keys_zone=STATIC:10m
 inactive=1d max_size=1g;

upstream { 
 (...)
}

server {
 (...)
 location / {
  include uwsgi_params;
  uwsgi_pass backendpython;

  uwsgi_cache STATIC;
  uwsgi_cache_key backend_python_cache_v1_$request_uri;
 }
}

Con esto configuramos una caché. Es bastante sencillo. Hay que tener en cuenta que Nginx sigue las instrucciones de las cabeceras Cache-Control, Expires, etc. Para que la caché funcione nuestro backend tendrá que enviar las cabeceras adecuadas.

Si no queremos esto (aunque yo lo recomendaría), se le puede decir a Nginx que ignore las cabeceras y cachee las respuestas según su código HTTP:

uwsgi_cache_valid 200 15m;
uwsgi_ignore_headers "Cache-Control";

Reiniciamos NGINX:

sudo systemctl restart nginx

Y probamos otra vez con siege. Si la caché es lo suficientemente grande, al cabo de unas cuantas pruebas el rendimiento debería subir bastante:

$ siege -f urls3.txt -c 8 -ib -t 60s
(...)
Lifting the server siege...
Transactions: 503930 hits
Availability: 100.00 %
Elapsed time: 59.63 secs
Data transferred: 567.13 MB
Response time: 0.00 secs
Transaction rate: 8450.95 trans/sec
Throughput: 9.51 MB/sec
Concurrency: 6.70
Successful transactions: 503933
Failed transactions: 0
Longest transaction: 0.04
Shortest transaction: 0.00

Ahora en mi caso se alcanzan las 8450 peticiones por segundo. Esto es un muy buen rendimiento. La tasa de transferencia sigue siendo baja, lo que confirma que hay que intentar la nueva API que empaqueta más.

Esto es un avance muy grande. Con esto se pueden servir datos dinámicos a una velocidad igual que los estáticos. Queda ver cómo evitamos servir un contenido cacheado con datos antiguos, pero eso lo veremos en otro post.

Nos vemos en la próxima entrega!

Diseñando una web ultra-rápida (III)

En el anterior post vimos una visión general de la arquitectura necesaria. Si no lo has leído aún, haz clic aquí. Si ya lo has leído, es momento de pasar a la acción!

Instalación

Como ya sabéis algunos, todos mis servidores son Debian GNU/Linux. Si queréis seguir los pasos, serían para Debian 9 (Stretch). Para más detalle, recomiendo la guía de NixCraft (en inglés).

Primero, instalamos el paquete “nginx“:

$ sudo apt-get install nginx

Esto instalará el servidor y lo pondrá en marcha con una web por defecto.

Si navegamos a http://127.0.0.1/, deberíamos ver la página de bienvenida de Nginx:

welcome-screen-e1450116630667

Si no veis la página de bienvenida, posiblemente tengáis otro servidor web funcionando como Apache o Lighttpd en el puerto 80. Si es el caso, la instalación con apt-get seguramente habrá fallado al iniciar el servidor. Podéis probar a detener los otros servidores web o cambiar nginx de puerto. También es posible que tuvierais restos de una instalación de nginx anterior y haya cargado la configuración anterior. En este caso se puede probar a purgar el paquete e instalarlo de nuevo.

Nginx en Debian se puede parar e iniciar con systemctl:

$ sudo systemctl restart nginx

La configuración del servidor está en /etc/nginx/nginx.conf, pero normalmente allí sólo hay cosas de configuración general y no hace falta modificar este fichero para nada. En mis pruebas este fichero no lo he modificado ninguna vez.

Normalmente modificamos /etc/nginx/sites-enabled/default, que si os fijáis es un enlace simbólico a /etc/nginx/sites-available/default. Es lo mismo que se hace con Apache.

Por defecto sirve los ficheros en /var/www/html. En servidores web lo habitual es crear otros ficheros (uno por web) en lugar de cambiar default, porque esto permite servir múltiples dominios en un servidor web. Para pruebas locales usar default es lo más sencillo porque nos permite visitar localhost en lugar de tener que configurar un dominio.

En mi caso lo que hice es copiar “default” a mi repositorio git, y hacer un enlace simbólico a él. De este modo los cambios los tengo controlados por git.

Configurando un backend

Por defecto nginx sólo sirve estáticos. Para servir contenido dinámico hay básicamente tres formas:

  • Módulo proxy: Pasa todas las peticiones a otro servidor web. Por ejemplo si alzamos un servidor de desarrollo en el puerto 8080, le podemos decir que todas las peticiones que nos lleguen las reenvíe allí. FastCGI y uWSGI son mejores alternativas.
  • Módulo fastcgi: Pasa las peticiones a otro ejecutable de nuestra elección, usando el protocolo FastCGI. Ésta es la mejor opción para servir PHP. FastCGI puede comunicar con otros procesos a través de sockets TCP, por lo que incluso podemos tener Nginx en una máquina y PHP en otra.
  • Módulo uwsgi: Pasa las peticiones a otro ejecutable, usando el protocolo uWSGI. Esta es la mejor opción para servir Python. Al igual que FastCGI, se comunica con sockets por lo que Python puede ser ejecutado en otras máquinas.

uWSGI fue diseñado originalmente para Python, pero a día de hoy soporta muchos otros lenguajes como PHP o NodeJS. Además, de los tres es el más rápido.

Como ya imaginaréis, yo lo voy a instalar con uWSGI y olvidarme del resto. Si vais a usar cualquiera de los otros dos, la configuración es prácticamente idéntica, ya que los tres módulos tienen los mismos comandos y opciones, sólo hay que cambiar el prefijo uwsgi por proxy o fastcgi.

Primero agregaremos el upstream en la parte superior del fichero:

upstream backendpython {
 server unix:///tmp/backendpython.sock; # por fichero
 # server 127.0.0.1:8001; # por TCP
}

Esto crea una variable “backendpython” y le asocia un servidor. Se pueden poner varios a la vez y nginx distribuirá la carga. La conexión puede ser un socket unix (por fichero) o socket TCP. Los sockets unix sólo funcionan en Linux/MacOSX/BSD. Los TCP funcionan también en Windows. Los sockets unix son más rápidos y más seguros, pero no permiten conectar a otra máquina.

En mi caso lo he conectado al fichero /tmp/backendpython.sock. Este fichero lo tiene que crear la aplicación uWSGI. En mi caso es un programa bash muy sencillo:

#!/bin/bash
test -f config.sh && source config.sh
export SCRIPT_NAME=backend.py
uwsgi_python3 -p6 --uwsgi-socket /tmp/backendpython.sock \
 -C777 -i --manage-script-name \
 --mount /api=backendpython:app \
 --py-autoreload 1

Esto lanza 6 procesos (-p6), nos crea el fichero de socket unix y le da permiso a todos (-C777). Para local está bien, pero en servidores habría que restringir los permisos.

También se puede hacer por TCP, o incluso HTTP. Dadle un vistazo a la documentación de uwsgi_python3 para ver cómo hacerlo.

Ahora hay que decirle a Nginx dónde tiene que usar este nuevo upstream. En la sección “location /” sería:

 location / {
 include uwsgi_params;
 uwsgi_pass backendpython;
}

Con esto ya lo tendríamos configurado. Hay que tener en cuenta que con esta configuración dejamos de servir los ficheros en /var/www/html. Todas las peticiones pasan por el backend. Esto es lo que yo quiero en mi caso porque mi backend sólo responde JSON. Para servir el frontend en Angular 6 agrego esta porción:

location /angular {
 location ~* ^.+\.(css|js)$ {
  access_log off;
  expires max;
 }
 try_files $uri $uri/;
}

En mis pruebas locales no me hace falta porque Angular levanta un servidor en el puerto 4200. (podríamos usar proxy_pass para redirigir este tráfico y así tener el autoreloader funcionando en el puerto 80)

Además, si estáis usando PHP, lo normal es que queráis servir los ficheros *.php en lugar de enviarlo todo a uno solo. Esto tiene una configuración distinta. Aquí hay una guía: Instalar Nginx con Php-fpm

Os recomiendo leer Pitfalls and Common Mistakes – NGINX que explica muy bien qué errores se suelen cometer al configurar el servidor. Incluso como advierte, hay muchas guías que cometen errores bastante grandes. Es de lectura obligada aunque estéis siguiendo una guía paso a paso.

Con esto ya tendríamos nginx funcionando con nuestro backend, pero en este punto aún no hay caché. Lo configuraremos más adelante.

Comprobando la velocidad de nuestro backend

Con el framework Flask me hice un pequeño servidor web que se conecta a MySQL y convierte los datos a json.

Con http2 la cantidad de consultas no importa tanto y no es necesario empaquetar recursos. Así que mi diseño inicial de la API son muchas urls con poco contenido.

Conseguí unas 2650 peticiones por segundo. Como uWSGI está alzando seis procesos, usa toda la cpu disponible.

La velocidad es muy buena, pero las peticiones son de 1kbyte o menos. La pregunta recurrente es, ¿qué es mejor desde el punto de vista de la cpu usada en el servidor? ¿Pocas urls grandes o muchas pequeñas?

Hay que entender que lo que intentamos conocer aquí es cuan eficaz es nuestro servidor web, cuántos recursos usará y no estamos viendo qué tan rápida es la web desde el punto de vista del usuario. Esto es a propósito. El objetivo es no gastar dinero en los servidores y poder tener tantos usuarios como quiera. Y aprovecharnos de sus dispositivos moviendo allí la lógica de la app.

Suena un poco egoísta pero es lo que hay. Hay quien hace minar criptomonedas a sus usuarios, comparado con hacer que usen sus recursos para mover nuestra web, ¿no parece tan malo, verdad?

Volviendo al tema, ¿qué tamaño de petición es ideal para nuestro servidor? Pues lo suyo es probarlo!

Dependiendo que que backend sea, framework y servidor web el resultado será distinto, así que recomendaría que cada uno haga sus pruebas y saque sus conclusiones. Yo hice las mías y éstos son los resultados :

registros bytes trans/sec kB/sec MB/sec
1 285 3364.2 931.84 0.91
2 584 3151.65 1689.6 1.65
4 1131 2995.12 3307.52 3.23
8 2279 2653.26 5908.48 5.77
12 3471 2374.81 8048.64 7.86
16 4549 2171.1 9646.08 9.42
24 7003 1835.11 12554.24 12.26
32 9025 1600.84 14110.72 13.78
48 14048 1271.97 17448.96 17.04
64 18114 1039.71 18391.04 17.96
128 36978 603.89 21811.2 21.3
256 73938 346.12 24995.84 24.41
512 147917 178.72 25815.04 25.21
1024 293412 86.62 24821.76 24.24
2048 381540 69.36 25845.76 25.24

grafica

Como se puede ver la cantidad de kBytes/s crece constantemente hasta los 16Kb y después se estanca en los 25Mb/s. La cantidad de peticiones por segundo empieza estancada y luego empieza a bajar a buen ritmo a partir de los 16Kb. (La escala de la gráfica es logarítmica en ambos ejes para facilitar la visualización de los datos)

Parece bastante claro que el punto clave son los 16Kb, donde se maximiza la cantidad de mensajes y la tasa de transferencia a la vez.

Para medirlo yo uso Siege. Es bastante completo y la carga que genera se parece bastante a la carga real de usuarios. No es el más rápido (es de los más lentos), pero los demás montan escenarios bastante poco realistas o les faltan funciones. Un ejemplo de comando sería éste:

$ siege -f urls.txt -c 8 -ib -t 60s

Las opciones que uso son:

  • -f: (file) Ruta a un fichero con una lista de urls a solicitar. El fichero es de texto plano con una url cada línea.
  • -c 8: (connections) Número de conexiones simultáneas. Para pruebas en local normalmente con sobrepasar un poco los núcleos del ordenador es suficiente. Para pruebas contra un servidor remoto puede que haya que subir el número bastante.
  • -i: (internet) Realiza las llamadas en orden aleatorio. Por defecto sigue el orden del fichero.
  • -b: (benchmark) Elimina los tiempos de espera entre solicitudes. Ideal para realizar pruebas de rendimiento.
  • -t 60s: (time) Establece cuánto tiempo va a durar el test. Se puede parar antes con Control-C, pero para resultados consistentes entre llamadas lo mejor es establecer un tiempo definido.

Con esto ya tenemos todo lo básico configurado y hemos hecho algunas mediciones interesantes. En el próximo post veremos cómo configurar la caché en nginx y medir la diferencia de velocidad.

Os espero aquí para la siguiente entrega!

Diseñando una web ultra-rápida (II)

Esto es una continuación del post Diseñando una web ultra-rápida (I). Si no lo has leído aún, te lo recomiendo. En él me planteo la posibilidad de que el contenido dinámico sea diseñado de forma que pueda ser cacheado como estático y por lo tanto, servirlo a toda velocidad.

En esta entrega quiero analizarlo un poco más a fondo y plantear un diseño inicial.

Primer paso, el servidor web

Como nuestro diseño se va a basar en servir estáticos lo primero es buscar un servidor web que pueda cachear como un reverse proxy y que sirva una cantidad ingente de peticiones sin consumir apenas recursos.

Mi elección aquí es NGINX. Es uno de los servidores más rápidos y es software libre. La instalación es sencilla, pocas dependencias y muy estable.

Si alguien se pregunta, ¿y porqué no Apache?. Pues tuve muy malas experiencias con Apache más de diez años. Ha mejorado mucho, pero no quiero ni volverlo a ver. Apache se come recursos de la máquina, se colapsa con aluviones de visitas, etc. Si alguien quiere probarlo adelante, yo personalmente paso.

Un benchmark Apache vs NGINX: Web server performance comparison – DreamHost

En mi máquina NGINX puede servir más de 8000 peticiones por segundo y aún veo el 50% de CPU libre. Es difícil estimar para mí correctamente la velocidad que tiene porque la mayoría de herramientas de benchmark consumen más CPU que éste servidor web.

El diseño del back-end y la API

Si nuestra página funciona con Javascript y consume datos JSON, entonces podemos aplicar caché a nivel de cada consulta separada que realicemos. Luego, en el diseño de los datos que va a recibir ésta web vamos a separarlos en tres tipos que yo defino como “public”, “restricted” y “custom”:

  • public: (públicos) Los datos a los que cualquier usuario, registrado o anónimo, tiene acceso a ver. En un foro serían los mensajes en foros públicos (casi todos), en un e-commerce los artículos y el catálogo. Como son públicos, todos los usuarios van a ver este contenido igual.
  • restricted: (restringidos) Son los datos que, con las credenciales correctas, se tiene acceso a ver. Todo usuario con credencial ve el mismo dato, de la misma forma. Y los que no, obtienen un 403 Forbidden y no pueden verlo. Es decir, es igual que public + autorización.
  • custom: (personalizados) Esto sería el contenido completamente dinámico, que cada usuario ve distinto. Esto podría ser por ejemplo una API para preguntar nuestra cuenta de usuario.

La mayoría de webs deberían poder acoplar su modelo de datos a éste esquema. Es más, en la mayoría de casos el 90% de los datos transferidos serán públicos, un 9% restringidos y un 1% personalizados. (Dependiendo del tipo de web, en Facebook el 90% es restringido). El problema es que todas las peticiones hoy en día pasan por el backend principalmente para la autorización, aunque todo el mundo pueda acceder a un recurso en particular.

Incluso cosas como un servicio de mensajes privados o email se puede plantear como restringido en lugar de personalizado, básicamente por dos motivos: Al menos dos usuarios (emisor y receptor) ven el mismo mensaje, y segundo, el contenido no cambia en cada petición ni según que usuario lo pida.

La lógica de esto es que la caché no puede ser utilizada por igual para todos los tipos de datos. Si todos los datos fuesen cacheados, entonces los usuarios anónimos podrían ver contenido privado accidentalmente o si saben la url. Además para las peticiones personalizadas se verían datos antiguos o de otros usuarios.

Para que esto funcione, las urls de nuestra aplicación tienen que apuntar a un único contenido y hay que eliminar todo el dinamismo para convertirlas en algo estático. Esto significa que no se pueden pasar parámetros para personalizar la consulta (filtros, orden, etc). Siempre que se consulte a una URL con la autorización adecuada debería devolver el mismo resultado, independientemente de quién lo consulte. Menos las personalizadas (custom) que siempre hay alguna, pero deberían ser la excepción y no la norma.

El primer problema es cómo servir la mayoría de URLs que no necesitan autorización, sin pasar por el backend para que verifique que efectivamente no lo necesitan.

Por el momento veo tres soluciones: (Se aceptan sugerencias!!)

1) Separar todas las url

Las APIs públicas estarían en /api/public. El resto en /api/restricted y /api/custom. En nginx definiríamos el comportamiento de estas tres.

Ventajas:

  • Clara separación de los contextos de autenticación y caché.
  • La eficiencia es máxima.

Desventajas:

  • Normalmente en la base de datos estos conceptos también están mezclados.
  • Publicar correctamente los contenidos puede ser complejo a veces y un desliz te puede publicar datos que no querrías. Si esto se tiene en cuenta al diseñar la base de datos, puede que sea ideal.
  • Los restringidos, ¿cómo se tratan?

2) Usar JWT (JSON Web Tokens)

Algunos servidores pueden usar JWT firmados digitalmente, verificarlos y leer su contenido. Esto lo podemos usar para que el servidor web pueda determinar si se puede acceder o no.

Ventajas:

  • Flexible. Se puede hilar bastante más fino.

Desventajas:

  • La extensión JWT para nginx es de pago.
  • Gran parte de los permisos de tu aplicación tienen que estar programados en NGINX.

3) Usar un servicio http sólo para la autenticación

NGINX permite enviar a un servicio http (que puede ser Python u otra cosa) las peticiones para autenticación. Este servicio sólo tiene que devolver respuestas HTTP 200 o HTTP 403. Sin ningún dato.

Ventajas:

  • No necesitas separar ninguna URL.
  • Toda la lógica de autorización reside en tu aplicación.
  • El contenido puede seguir siendo cacheado.

Desventajas:

  • Más lento, pues todas las peticiones pasan por tu backend. Aunque el proceso es mínimo porque no servimos contenido.

A tener en cuenta:

  • Se puede usar otro lenguaje de programación que realice esta tarea más rápido.
  • Se puede separar parte del contenido público en /api/public para más rapidez.

Conclusiones

La primera opción planteada era la más evidente, pero la última usando un servicio aparte se vuelve cada vez más atractiva para mí. Además, que se pueden mezclar ambas libremente.

Al final he hecho algunas pruebas con Python+Flask y NodeJS.

Con Flask he conseguido 2400 peticiones por segundo para respuestas que apenas contenían el HTTP 200, usando los 4 núcleos físicos de mi i7. (Comparado con las 8000 de Nginx, no está mal. Pero Nginx no saturaba mi CPU y Python saturaba todos los núcleos a la vez)

Con NodeJS y un servidor muy simple he conseguido 3500 peticiones por segundo en condiciones similares. Pero usando sólo un único núcleo físico. He probado soluciones de multiproceso, pero al final me sale el mismo rendimiento pero usando más CPU. Parece que la comunicación entre procesos toma más tiempo de CPU del que gano. No obstante, es planteable con esa velocidad el usar sólo una CPU.

Otra opción para NodeJS sería abrir cuatro servidores HTTP en cuatro puertos distintos y enviar distintas URL (por patrón) a los distintos puertos. Un balanceo de carga bastante malo, pero funcionaría. Me he planteado también un balanceador de carga delante, pero a no ser que tengamos varios equipos para la web, me parece que agregar otra pieza de software no va a valer la pena. ¿Alguien recomienda un balanceador para que lo pruebe?

Y finalmente, cabe plantear que las respuestas de éstos backends también se pueden cachear por nginx. Tiene instrucciones para hacer caché en la autenticación. Habría que añadir las cookies o alguna cabecera para no mezclar usuarios. El principal problema es que muchas URL van a requerir la misma autenticación y cachear todas las URL multiplicado por todos los usuarios huele a ineficaz. Habrá que ver cómo hacer que Nginx entienda que ciertas URLs son iguales, y que en cuanto al usuario sólo tiene que mirar cierta credencial.

De momento lo dejo aquí, creo que va cogiendo forma y se vuelve más interesante. Espero publicar una continuación pronto!

Diseñando una web ultra-rápida (I)

¿Qué sucedería si pudiésemos hacer una web que sirviese 10.000 peticiones por segundo en un buen servidor? ¿Atraería más usuarios? ¿vendería más?

Posiblemente no haya casi ninguna diferencia, pues casi todas las webs sirven menos de 5 peticiones por segundo. Exceptuando las grandes, todas las demás no necesitan de grandes servidores ni complicados sistemas, hasta que se hacen muy grandes y necesitan más servidor. A partir de ahí, pagan más al mes por tener una mejor máquina y siguen funcionando.

Y ahí es donde reside la diferencia. Si nuestra web está bien hecha y es tan rápida, lo que pasaría es que dejaríamos de contratar un servidor de 100€ al mes a contratar uno virtual de 10€ al mes. A lo mejor sólo sirve 50 peticiones por segundo, pero el servidor es tremendamente barato. Y cuando hace falta más rendimiento, se sube un poco y enseguida el problema se soluciona de nuevo. Y hay servidores incluso más baratos, por lo que el coste de mantenimiento puede ser ridículo. Llega un punto que ya no sabes si estás pagando por dominios o servidores.

He estado estos días analizando cual sería la forma de poder montar una web que consuma muy pocos recursos y que pueda gestionar una gran cantidad de usuarios.

La receta es bien conocida y no tiene nada de nuevo. Como todo en programación, para que vaya más rápido hay que conseguir que trabaje menos. Cuantas más funcionalidades y más dinamismo, más trabajo se le da al servidor y por lo tanto, más lenta.

Por lo tanto la más rápida es un index.html estático que diga “Hola mundo” y ya está. Ni imágenes ni tonterías. ¿Cuántos usuarios puede servir? ¡Un porrón!.

mic-drop-2boom

Que sí, que lo sé.

Es obvio que si quieres vender o hacer cualquier cosa con una web hoy en día, publicar un html estático no es sólo ridículo, es que es obvio que no sirve para nada.

Pero no es tan obvio. El contenido estático es el que más rápido se puede servir desde una web. Si es pequeño mejor.

Hoy en día es una locura pensar que hayan usuarios que accedan a la web sin tener Javascript. Y el Javascript entre los distintos navegadores es bastante bueno; desde luego nada que ver con la época del Internet Explorer 6.

Piénsalo bien, por muy dinámica que sea la página, por mucho que cada usuario vea algo radicalmente distinto, ¿Cuántas veces sirves el mismo trozo de texto a todo el mundo? ¿Una o dos veces? Yo diría que cientos o miles. Incluso Facebook, donde cada usuario ve algo completamente distinto, el mismo post se ve cientos de veces y se envía incluso varias veces al mismo equipo en el transcurso de los días. Y la mayoría de las webs no son como Facebook. ¿Es ese post contenido estático o dinámico? Si cuando alguien lo escribe, lo guardo en disco y lo sirvo desde allí, ¿es dinámico o estático?

Por supuesto lo de grabar en disco cada mensaje y leerlo luego es una tontería bastante grande, sólo es un ejemplo. Si alguien se lo pregunta, es bastante más lento. Las bases de datos están mucho mejor preparadas que los sistemas de ficheros para ésta tarea y la realizan más rápido.

Pero si hablamos de webs SPA (Single Page Application) la cosa ya cambia. Si el contenido está en caché y el navegador consulta directamente éste, la carga para el servidor web es ínfima (con la excepción de que la cantidad de ficheros requerida es tan alta, que seguramente tampoco compense).

Esto es un poco la cruzada que llevo detrás bastante tiempo, poco a poco probando distintas cosas y viendo el resultado y sus posibilidades. Sólo es una prueba de concepto, nada más; pero es interesante y vale la pena compartirlo.

La idea es como sigue: En lugar de servir cada respuesta dinámicamente, cambiemos el diseño radicalmente para que se pueda servir estáticamente, desde una caché en el servidor. Esto significa que para casi todas las URL de la aplicación, cuando se realiza una petición GET, tiene que devolver siempre el mismo resultado independientemente de usuario o permisos. También significa que no se pueden pasar parámetros para personalizar consultas. Un recurso tiene que aparecer siempre en la misma URL con los mismos parámetros y no debe haber más que una forma (o dos) de acceder.

Al final se trata de mapear la base de datos que tienes a APIs REST. Habrá una cantidad finita de url’s que descargar, y si las descargas todas, acabas de exportar toda la web a disco.

Una de las grandes ventajas es que si el servicio de Python (o PHP, etc) se cae, la caché puede seguir teniendo efecto y los usuarios pueden seguir navegando. Excepto cuando intenten enviar cambios (POST/PUT/etc), la web parecerá plenamente funcional.

Hay un montón de problemas de diseño en esta idea, pero poco a poco parece que se pueden solventar. De momento he conseguido 8000 peticiones por segundo para contenido que puedo cachear, 3000 para el que requiere autenticación y 1000 para el que es completamente dinámico en un i7 920 @2.66Ghz. Iré actualizando en nuevas entradas cómo hacerlo, qué problemas he ido encontrando y qué soluciones me han parecido las más apropiadas para cada caso.

Hasta el próximo post!