bmitc
I have a new role in a domain that is completely dominated by Python. This is in the scientific R&D domain, where Python is used for everything from distributed control systems to data analysis pipelines. Of course, it’s the latter that makes some sense but not the former. However, that is the ultimate reality of the situation, in the short term at least.
I have never built big systems in Python before, so I am feeling a little bit like a fish out of water, especially since most of my systems have been built using Elixir, F#, and LabVIEW, all using actor/process-oriented architectures.
What I am looking for is advice from people who have built large systems in both Python and Elixir or Erlang and how they went about doing Erlang/Elixir-y type of things in Python without straying too far away from so-called Pythonic code. Obviously, a distributed control system calls for exactly the things provided by the BEAM, Erlang/Elixir, and OTP, but in the early days here, I must build in Python. So what I need are BEAM and OTP-ish type of things in Python without being too exotic. I.e., I need this to be robust as possible in Python while having a reasonable architecture for handling asynchronous and distributed things.
What architectures did you use? What libraries? Etc. Think of large amounts of hardware being controlled with large amounts of telemetry coming in from distributed network devices, all collected into a single control computer that will also handle user interfaces. I know of Pykka, and I’ve done some experiments with it, and it looks reasonable. But I am wanting some advice of robust frameworks, and I don’t know how robust and well-used Pykka is. There is also Ray. The user interface(s) will be using PyQt6 or PySide6, although there will also likely be some remote viewing (over the browser) for telemetry only.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security










Showing Posts 1 to 8- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
RomanKotov
Hi,
I have both Python and Elixir experience. Other languages greatly influenced me with their ideas. Beam VM and OTP inspires me with architecture decisions. Its documentation is very proactive and helps to see the problems from other perspective.
I have never worked with Pykka directly, only when debugged code in Python music player. This experience is not enough, so will not speculate on it.
As I understand, you have a server, that collects telemetry data. This server also needs to display interface for the user. It would be helpful, if you could give more context about the task. For example:
I would split the task into 3 main parts. Receiving the data, processing it and presentation.
Receive (devices send data to a server)
If I expect large amounts of data, then I possibly need to store it raw before processing.
There are some ways to do it. For example, the data can be saved directly to a database (many options here), added to a queue (RabbitMQ, Redis, Kafka, etc) or possibly some other solution.
It is better to have these endpoints as fast as possible, so they can handle much data. Also I would load-test different solutions here. Possibly web servers with async features will work fine.
We can compare it to process mailbox in Elixir. A process receives many messages, but can handle them as soon as gets some processor time. So, we try to emulate a mailbox here.
Receive (server polls devices for data)
If server fetches data from many devices over the network, then this part should be as concurrent, as it can be. Running multiple Python processes will introduce overhead here, so possibly you will want to go with threads, or even asyncio-related libraries. Idea is the same - you need to fetch as many data as possible with as little overhead, as possible.
You can compare this to launching multiple processes in Elixir to get the data, but we try to emulate the same behaviour with features from Python.
Process the data
If you need to show a raw data, then you can omit this step at all.
If you need to process it somehow, then you can look at something like Celery. I have also worked with Apache Airflow, but it can be overkill here. So, the basic idea is to get a chunk (batch) to process and store the results somewhere. The details greatly depend on the use-case.
Present data
It depends on the type of data, its amounts etc. Possibly you can just generate pages with charts via Flask or Django templates. Besides, you can just refresh a page every N seconds instead of using websockets. It can be just refreshing the whole page, only a part of it, or fetching data from API. Example with Stimulus, example with unpoly.js, example with HTMX. I would like to have only one UI. If you do really need a desktop application - possibly there is a way to show the same UI in a web-view.
Also there is Plotly Dash - have used it to render dashboards. It also supports open-source version.
General advices
I would recommend Designing Data-Intensive Applications book. It gives nice overview of tradeoffs, architecture details and approaches to work with data. It does not dive too deep into details, but gives many ideas (at least for me). Enjoyed reading it (and watching nice pictures there).
Another book with useful tips for overloaded system is Erlang in anger. It describes some typical issues with overloaded system in Beam and how to fight them. But you can apply ideas in it in other languages. Actually you can use actor model to design the system, but it will consist in Operating System processes instead of BEAM VM ones.
I would also recommend not to make the system too complicated - you will be grateful for it later. Possibly the simplest solution will work fine for you. The more moving parts you have - the more of them you need to support.
Hope it will help you, or at least give some ideas.
bmitc
Hi Roman,
I apologize for my delayed response. Thank you very much for such a detailed response and helpful pointers! I read it at the time you replied and kept doing some research. To provide a little more detail, here is a kind of architecture I’ve landed upon:
However, even that is a little bit of an old design, because as of yesterday, we no longer have a need to separate the GUI over a network, so it’s likely that the WebSocket server is no longer needed and can become some sort of internal worker process.
There are indeed a lot of external devices, primarily TCP/IP servers. So I have been landing on the idea of using
asynciofor all (or at least most of) the TCP/IP requests withasyncio.Queues for message passing, treating the variousasynciostreams clients as little workers. I think you are right that creating many Python processes introduces a lot of overhead (because of the additional network calls between the processes) and orchestration (because of having to rely on the OS to manage these) needed. And it also requires the need for serialization/deserialization between the processes, instead of using native Python datatypes and of course custom ones usingdataclassesandtyping.NamedTuple.Between
threading,asyncio,multiprocessing, and separate Python processes, I mainly landed onasynciobecause of the very nice APIs it has (such asasynciostreams). For athreadingsolution, I probably would lean on Pykka since it is the actor model on top of Python’sthreading, but I worry about the context switching performance hit. Formultiprocessing, I considered a queue-based solution there, but I am unfamiliar withmultiprocessingfor IO-bound applications, andasyncioseemed to be the recommendation there. And as mentioned, the multiple, separate Python processes feels unwieldy, slower, and requires serialization/deserialization for datatypes.Does that sound accurate to you? Or a valid approach and decision criteria?
There is some processing, but it is mainly parsing the text-based responses into datatypes and then passing them around to a timeseries database and the GUI. There may be one or two procedures that we need with heavier calculations, but even there it shouldn’t be too much to handle. If so, then perhaps
multiprocessingis the way to offload the calculation. The aggregation is primarily simply collating the data and then displaying and saving it. There will be some watchers that look for values out of bounds that then provide alerts. But such calculations are like “if outside of a given numeric range, then alert”.I think in general, I have ruled out the need for and complexity of things like RabbitMQ, Celery, etc.
Thank you for mentioning that. I am aware of Plotly, having used it in F#, but I haven’t ever used Dash or looked into it much. Right now, the plan was to write data to a timeseries database (such as InfluxDB, QuestDB, or TimescaleDB), and then view that data with Grafana. How does Dash compare to that approach?
Thank you again, Roman! I greatly appreciate the time you put into your original post.
RomanKotov
Hi thanks for a reply! I am glad that it helped a little bit
So, as I understand you have something like this:
According to your description, it is not an ordinary HTTP server. I will be able to provide only general advice in this case. Possibly it will give you some insights. You know the requirements better and will be able to decide how to structure the system.
Regarding multiprocessing.
Here is a nice article about different ways to speedup Python code.
Types of tasks:
Difference between processes and threads in Python:
Types of IO:
Major part of Python code and libraries use blocking IO (at least now).
So, here is a difference between between multithreading, multiple processes and asyncio:
I gave a very high-level overview of how Python system works. If you have only one process, and some part takes too much CPU time - the whole system may freeze and stop responding. I would not recommend to run many background tasks. Also each process has a single GC - it can pause the whole system if it consumed too much memory. Sometimes Python leaks a memory, so GC is possible for active long-running processes.
Regarding Celery
Celery runs background tasks or . It starts a couple of “worker processes”. Each worker process subscribes to a “broker” - message queue (example brokers are Redis or RabbitMQ). Worker reads a task from the queue and processes it. You can put the task to the queue at any part of your application (even from another task). “Celery beat” may run scheduled tasks (like cronjob).
Regarding message passing between processes
I am not sure that I understand the reason to pass some datatypes between processes. Possibly there is a simpler approach.
Regarding Plotly Dash
I think it will be better to use Grafana here, if it fits you. Plotly Dash allows you to write dashboards in Python code. Used it for small-medium datasets, so have no experience if it scales well and fits your needs. Suggested it as an option (not as a strong recommendation), so you can try it.
My thoughts about possible architecture
Warning - this is not a call to action, but only a point of view.
As I understand, the task is to:
I think the system may consist of this parts:
If you need a realtime updates, then you may create a message queue (possibly consider external one). If a connector sees the outlier, it publishes the id of the outlier into the “outliers” topic. Alert system reads this topic and takes does relevant actions.
So the algorithm like this:
As I understand, you have a very similar solution:
Again, you know your system, requirements and constraints better than me, so please, do not take any action unless it makes sense to you. My message is just an opinion, not an order or call to action. I can not read minds or debug systems/architecture without seeing a code.
I wish you to succeed with your system. Possibly this message was helpful for you.
RomanKotov
I thought a little about actor system. It is very difficult to create generic one, like in Erlang. But you can use basic actor primitives.
I tried to implement a simple actor implementation on top of asyncio. Possibly it will be help you with some ideas. I do not urge you to integrate it into the codebase
By the way, if you will need to split the load between multiple processes, you can do it with something similar to the approach a
mix testuses to partition tests. Basically, you can split a list of remote servers according to their index. This may help to utilize the number of CPU cores. I would recommend to have only a single process locally - it will help to debug issues easier.bmitc
Hi Roman. Thanks again for your messages and advice. I’m just now getting back to your latest message, and it’s very interesting because it looks like we independently came up with some very similar things, which I suppose is not a surprise given the familiarity of messaging systems that Elixir/Erlang brings. Here’s a prototype I recently built that creates the concept of a worker (I avoided the term actor because of some non-technical reasons, although I like the term actor) which contain inboxes that wrap
asyncio.Queues for messaging. Here’s the Python discussion forum post where I describe the prototype in more detail: Request for review of PySide6 (Qt for Python) andasynciotask prototype - Async-SIG - Discussions on Python.orgRomanKotov
Hi,
The Elixir/Erlang systems are very nice - you can apply their ideas in other languages too.
I have recently realised, that the author of Python
gunicornweb server is very closely related to Erlang ecosystem and has popular libraries here too. Gunicorn uses the analog of supervisor to increase a number of workers and process more requests. Supervisor watches the workers and restarts them once they crashed, or after a certain number of requests (--max_requestssetting). This allows to reduce memory leaks of long-running Python processes.By the way, you can consider using
asyncio.PriorityQueueinstead ofasyncio.Queue. This can help you to add “signals” to your workers. Signal is a more urgent message. If you don’t have priorities, the urgent message will wait until all previous messages are processed. With priority, it will wait only the completion of processing of your current message.I have looked into your example - it is pretty nice!
Regarding the scaling:
ulimit -n), if you want to open thousands of connections from the single process. Otherwise you will get something likeToo many files openerror.Mix.install(examples) together with ThousandIsland. You can deploy this server to other PC and open as many connections as you wish (or as your system allows). This will be the best proof if your approach scales well and will allow to find issues with the design.ulimit -nlimits (but it is better to verify it during a test). Each worker possibly will need to speak only to the “supervisor” and not to other workers. You can partition data between workers asmix test(a sole link in my the previous message).htopor similar application. You can periodically measure amounts of RAM (as well as other useful metrics) and print them into stdout in a CSV format (just separate them with commas or other delimiters). You can store the results in a file withsimulation.py > outputs.csvorsimulation.py | tee output.csv(if you want to see the logs in the shell too). You can load the results in Excel, and create charts from it. By the way, you can print different data tostderrandstdout. For example, you can usestderrfor debugging information, andstdoutfor metrics, or vice versa.bmitc
To potentially transition this away from Python and into more Elixir-land, as I think Roman and I have come up with a pretty good “core to Python” way of doing actor/process stuff with Python’s
asynciothat’s ran its course, I do have a question at the end regarding Thousand Island vs Ranch for building TCP/IP servers.Yea, I considered that, but I generally like to avoid priority queues. However, the one use case that I can consider is using higher priority messages for core/framework messages like “stop”, leaving normal messages available to other workers as normal priority where they can’t set priority of the messages.
And thank you for taking a look at the example. It will keep getting refined, but I am actually liking it. It feels quite process/actor-like, even though everything is in a single-thread, which much be remembered. Because like you said, running a long-running synchronous message will halt the entire thing.
Python’s
threading,asyncio, andmultiprocessingmodules plus the litany of third parties libraries are the perfect example of just how complex Python is. Because in Elixir/Erlang, all of these things are just processes, and there’s no need to worry about threads, schedulers, etc.I’m still trying to avoid multiple processes, although
asynciodoes have the ability to useasyncio.run_in_executor, using aProcesPoolExecutor. The reason is to mainly avoid the weirdmultiprocessingqueues and pickling (serialization/deserialization) behavior that could occur for more custom data types. This is in particular since I adopt a rather thorough type-driven or domain-driven programming style.For an Elixir-specific question, do you or others have some thoughts on why one should use Thousand Island over Ranch? I have indeed spun up little test servers in Elixir for testing clients in Python, but I was using Ranch. It took a bit to convert the user guide examples in Ranch to something I understood in Elixir. Is Thousand Island as heavily used as Ranch? Is it supposed to be easier? The Ranch documentation is a bit terse.
RomanKotov
Regarding multiprocessing.
Elixir/Erlang can utilize all CPU cores and resources from a server. Standard Python is single-threaded. This means, that even if you have a beefy server with lots of CPU cores, it will use only one of them by default. I do not try to push you to use
multiprocessingor other things. Just looking at it from resource utilization perspective. It is fine, if your server runs other applications except Python server - they will use some CPU.I remember I load-tested a poorly performing Node.js application. We bought larger server for it, but it did not fixed the issue. It turned out, that the application used only a single core, and was limited by RAM. We tweaked it and the application started to use more resources.
My goal is not to force you to use multiprocessing, but to point to possible limitations. I think there are ways to use multiple processes (to use more resources and scale better) and not to use queues from multiprocessing at all. I would think if it is possible to partition all clients between multiple OS processes (not Python ones) without sharing any memory between them. I like thinking about the possible ways to scale application and creating architecture with this in mind (even if I will not use this option in future).
It is up to you whether to do something about it. Plan not to continue digging into this topic.
Regarding Thousand Islands vs Ranch
Both libraries heavily use standard
:gen_tcpor:sslmodules from BEAM. Thousand Islands is heavily inspired by Ranch and use similar ideas. It is rewritten in Elixir and has less code (easier to understand).I believe you can do the benchmark using any of these libraries, and it should work fine. But it may be slightly easier to configure and reason about the code from Thousand Island. This library is newer, so it is not so I believe it is not so heavily used as Ranch right now. More recent Phoenix versions migrated to it by default.
I had to tweak
:gen_smtponce, and it relies on Ranch. It was quite difficult to dig into the Ranch codebase and understand what is going on.If you already have Ranch-backed test service, then it is better to continue using it. If you want to create a small throwaway script to load-test your implementation - why not to try Thousand Island?
Personally I don’t have strong preference from one library over another. I believe both ones have edge cases.