How do you build Python applications that can handle literally billions of requests?
It certainly has been done to great success with places like YouTube,
handling a million requests a second, and Instagram, as well as internal pricing APIs
at places like PayPal and other banks. While Python can be fast at some operations and slow
at others, it's generally not so much about the language raw performance as it is about
building an architecture for that scale. That's why it's great to have Julian Danzhao on this show.
We'll dive into his book, The Hacker's Guide to Scaling Python,
as well as some of his performance work he's been doing over at Datadog.
This is Talk Python to Me, episode 312, recorded April 8th, 2021.
Welcome to Talk Python to Me, a weekly podcast on Python, the language, the libraries, the ecosystem,
and the personalities. This is your host, Michael Kennedy. Follow me on Twitter where I'm at,
mkennedy, and keep up with the show and listen to past episodes at talkpython.fm,
and follow the show on Twitter via at Talk Python.
This episode is brought to you by 45 Drives and us over at Talk Python Training. Please check out
what we're offering during those segments. It really helps support the show. Julian,
welcome to Talk Python to Me.
Thank you.
It's great to have you here. I think we've got a bunch of fun stuff to talk about. It's really
interesting to think about how we go about building software at scale. And one of the things that just,
I don't know how you feel about it, reading your book, I feel like you must have some opinions on
this. But when I go to a website that is clearly not a small little company, it's obviously a large
company with money to put behind professional developers and stuff. And you click on it,
and it takes four seconds for every page load. It's just like, how is it possible that you're
building this software with so much? Oh, this is the face of your business. And sometimes they decide
to fix it with front end frameworks. So then you get like a quick splash of like a box with a little UI,
and then it says loading for four seconds, which to me feels no better. So I don't know, I feel like
building scalable software is really important. And still people are getting it quite wrong quite often.
Yeah, I mean, I think it's all also, there's a lot of things that you want to do when you do that,
which is like, I mean, write proper code for sure. But also you want to be able to
meter everything, like to understand where the bottleneck might be. And that's not the easy part,
like writing code and fixing bugs, we all know to do that. But then if we are asking you to optimize,
that's one of the things that I usually use as an example when I talk about profiling is like,
well, if I were to ask you tomorrow, like I want you to tell me which part of your code is using
20% of the CPU, you really don't know. Like you can guess, you can probably do a good guess most of
the time. But for real, you don't know, you have no clue until you actually look at the data,
use a profiler or any tool for that being that will give you this information.
Yeah, we're really bad in using our intuition for those things. I remember the most extreme example
I ever had of this was I was working on this project that was doing huge amounts of math,
wavelet decomposition, kind of like Fourier analysis, but I think kind of worse. And I thought,
okay, this is too slow. It must be in all this complicated math area. And I don't understand the
math very well. And I don't want to change it. But this has got to be here, right? It's slow.
And I put it into the profiler. And the thing that turned out was, we were spending 80% of our time
just doing and like finding the index of an element. Yeah, in a list.
Yeah, which is not obvious at all.
And we it's insane.
Yeah. My favorite, I think, programming quote is from Donald Knuth, which is,
value optimization is the root of all evil. Like it's, yeah, widely known. I mean, I think I will
quote it every week or so now.
Yeah, it's fantastic. It's fantastic. Yeah. In my case, we switched it to a dictionary and it went
five times faster. And that was it. Like it was incredibly easy to fix. But understanding that
that was where the problem was, I would have never guessed. So yeah, it's hard to understand. And we're
going to talk about finding these challenges and also some of the design patterns. You've written a
really cool book called The Hacker's Guide to Scaling Python. And we're going to dive into some of the
ideas you cover there. Also talk about some of your work at Datadog that where you're doing some of the
profiling stuff, not necessarily for you internally, although I'm sure there is some,
but it also could be, you know, for so many people, like you guys basically have profiling as a service
and, you know, runtime as a service, runtime analysis of the service, which is great. And we'll get into
that. But before we do, let's start with your story. How do you get into programming in Python?
That was a good question. So actually, I think I started like 15 years ago or so. I actually started
to learn Per, the first programming language, like, you know, kind of scripting language,
like we used to call them at least a few years ago. And I like Perl, but I wanted to learn like
object-oriented programming. And I never understood Perl object-oriented programming. Like their model
was so weird for me, maybe because I was young and I don't know. Somebody talked to me about Python.
I bought the book, like the Auree book about Python. And I kept it around for a year or so because I had
no project at all, like no idea. Most of my job back then was to be a sysadmin. So not really anything
to do with Python. And someday I'm like, I was working on DPN, like the distribution. And I was
like, oh, I need to do something like a new project. And I'm going to do that with Python. And I started
to learn Python this way with my project on one side, the book on the other side. I was like,
that's amazing. I love it. And I never stopped doing Python after that.
Yeah, that's fantastic. It feels like it very much was a automate the boring stuff
type of introduction. Like there's these little problems and bash is too small or too hard to make
it solve those problems. What else could I use? And Python was a good fit for that.
Yeah, it's a great way. I mean, usually I have a lot of people coming to me over the years and
being like, Julian, I want to contribute to a project. I want to start something in Python.
Like, what should I do? And like, I don't know. Like, what's your problem you want to solve?
Like if you want to find a boring thing you want to automate or anything, that's the best idea you can have to
if it's an oposal project that exists already. Great. I mean, good for you. It's even better.
But I mean, just write a script or whatever you want to start hacking and learning. That's the best
ways to scratch your own itch. Yeah, absolutely. It's so easy to think of, well, I want to build this
great big thing, but we all have these little problems that need solving. And it's good to start
small and practice small and build up. And I find it really valuable. People often ask me like,
oh, I want to get started. What should I do? Should I build a website like this? Maybe machine learning
thing like that? I'm like, whoa, whoa, whoa. Like that's, yes, you definitely want to get there,
but you're really, really just starting. Like, don't kill yourself by trying to take on too much
at once. So yeah, it sounds like it worked well for you. How about now? What are you doing day to day?
I hinted at Datadog. Yeah. So I've been doing Python for the next 10 years. After I learned Python,
I've been working on OpenStack, which is a huge Python project, implementing an open cloud system
where you can host your own AWS basically. And so everything is in Python there. So I work on very
large, one of the largest, I think, Python project, which has opened the site for a few years. And then
I decided to go for a change. And Datadog was looking into building a profiling team,
building a profiler, a continuous profiler, which means you would not profile your script on your
laptop, but you would profile your application running on your production system for real.
And I was like, that's not something I think anyone did before in Python at least. So I want to do that.
And that's what I started to do like two years ago. And I'm still doing that.
That's really interesting because normally you have this quantum mechanics problem with profilers and
debuggers, especially profilers, like the line by line ones so much where it runs at one speed normally,
and then you hit it with like C profile or something, and it's five times slower or whatever
it turns out to be. And you're like, whoa, this is a lot slower. Hopefully it gives you just a factor
of slowness over it. Like if it says it's been 20% here and 40% there, hopefully it's still true at
normal speed. But sometimes it really depends, right? Like if you're calling a function,
that goes out of your system and that's 20%, and then you're doing a really tight loop with lots
of code, the profiler will introduce more overhead in your tight loop part than it will in the external
system where it adds basically zero overhead. And so that's a big challenge of understanding profiling
results in general. And it's a really big reason to not just run the profiler constantly in production.
Right?
Yeah, exactly. And people do that now. I mean, if you have the right profiler, so the way I see
profile works, I mean, and we can dig a bit into that, but like C profile, the way it works,
like it's going to intercept everything. It's what we call a data mystic profiler, where if you run the
same program twice, you will get the same C profile output for sure. Like it's intercepting all the
function calls that you have. So if you have a ton of function calls, it makes things like you were
saying five times slower for sure, at least. So yeah.
Yeah. And it'll inject a little bits of bytecode at the beginning and end of every function, all sorts of
stuff. And it actually changes what happens, right?
Yeah, exactly. So you can change the timing. It can change. I mean, it's so it's a good solution to like
ballpark estimate of what's going on and it gives you pretty good results. And usually it's a good tool.
like I use it a lot of times over the years and it always gave me a good information. The problem with
C profiles is that you can't use in production because it's too slow. It's also not providing
information. Like it gives you the whole time that you use, but not necessarily the CPU time of each of
your thread, et cetera, that you're going to use. So the information is not really fine grained. It's
a rough whole time.
Yeah. It's probably not streaming either. Right. It probably it runs and then gives you the answer.
Exactly. It's not some sort of real time stream of what's happening.
So, I mean, for one of the kids, like we were mentioning previously where, I mean, you know,
this part of the code, like this scenario that you can recreate in a one minute script or something,
you know, it's slow and it should take only 30 seconds. You can run C profile around it on one
minute on your laptop and be, okay, I'm going to optimize this piece of code. But if you want to see
what's happening on production with a real workload for real and like you were saying,
streaming the data to see in real time what's going on, or C profile doesn't fit. And also
any data mystic profiler, which tries to catch everything your program does will not work with
good performance. So you have to do another approach, which is what most profiling profilers for
continuous profiling do, which is statistical profiling, where you actually sample your program
and you try to look what it does most of the time and the most often. So it's not a true
representation. It's not like the reality 100%. It's a good statistical approach of what your
program is doing most of the time.
I see. Is that more of the sampling style of profilers where it's like every 200 milliseconds,
like, what are you doing now? What are you doing now?
Exactly.
Like a really annoying young child. Like, what are you doing now?
Exactly.
And it's going to miss some things, right? If there's a function you call and it's really quick,
it's like, well, you never called that function as far as the profiler is concerned,
because it just didn't line up. But if you do it enough over time, you'll get to see a good picture
of exactly. You don't care about the stuff that happens really fast, but you care about the stuff
that happens really slow. And those are going to show up pretty largely in these sorts of sampling.
Exactly. So if you see profile, you will see this very small function because it catches
everything. But reality for the purpose of optimizing your program, you actually don't care.
You don't care if you don't see them statistically, but because they're not important. So that's not
what you want to optimize in the end. That's not where your problem lies, probably. It's in the
like outliers, the one that you see often in your profile, the one using, you see like 80% of the
time when the profiler asks your program, what are you doing? It's always the same function being called,
what's the one you want to look at?
Yeah. So I think that fits quite well with the production style. I know I was going to ask you about
your book, but we're sort of down in this profiling story.
That's fine.
And you know, I've used Datadog's tools for error handling and like exception, you know,
let me know when there's an error type thing. So I have that set up on like Talk Python,
the podcast site and the Talk Python training courses site. And of course, when you turn it on,
you get all these errors that are like happening in the site, but nobody's complained to you that
you didn't realize there's some edge case or whatever. And it's really fantastic. But something
I've never looked at is the real time profiling stuff that you're working on these days. So maybe just,
I have no idea what this is like, I can imagine maybe what it's like, but you know,
give me a sense of what kind of stuff do I get out of it?
Sure. Yeah. So what you'll get, the first thing you'll get is to profile. So you'll get flame charts,
essentially, which are, you know, this kind of charts where, I mean, they look like flames,
usually because they are like orange and red and going up and down and the height being the depth
of your stack trace and the width being the percent of time of resources that you use. So usually it's
time you're going to meter. For example, we meter a wall time in CPU. So what you'll see is if your
function using a lot of wall time, is it waiting for something? Is it waiting for a socket to be read,
for a lock to be acquired? But one of the other profile we gather is how much CPU is actually using.
So if you want to know if your program is CPU bound, you will see which function is actually
using the most CPU in your program.
Right. Because I could go to like my hosting provider and I could check a box and say, no,
no, I don't want to pay $20 a month. I want to pay $50 a month to make this go two and a half times
faster. If I'm CPU bound, that might actually work. But if I'm not, it probably has no effect or small
effects, right?
Exactly.
This portion of TalkPytherMade is brought to you by 45Drives. 45Drives offers the only enterprise
data storage servers powered by open source. They build their solutions with off the shelf hardware
and use software defined open source designs that are unmatched in price and flexibility.
The open source solutions 45Drives uses are powerful, robust, and completely supported from
end to end. And best of all, they come with zero software licensing fees and no vendor lock-in.
45Drives offers servers ranging from four to 60 bays and can guide your organization through
any sized data storage challenge. Check out what they have to offer over at talkpython.fm
slash 45Drives. If you get in touch with them and say you heard about their offer from us,
you'll get a chance to win a custom front plate. So visit talkpython.fm/45Drives or just click
the link in your podcast player. So knowing that answer, knowing that would be really helpful.
Can I scale this vertically or do I have to change something else? Yeah.
Yeah. It's a whole story on profiling where, I mean, most of our users, when they come to us,
like we save thousands of dollars because we actually understood that we got a button like
here or there and we were able to downsize our deployment because we optimized this function
and we understood that this was blocked by this IO or whatever. And when you understand all of that
with profiling, whatever the language is, by the way, and being Python or Java or anything,
you actually save a lot, a lot. We have terrific stories about customers or internal users
saving thousands of dollars just because they were able to understand what was going on in their
program. And scaling up was not the solution, optimizing the right function was the solution.
So yeah, you'll get CPU, WorldCharm, and we also do memory profiling. So you'll see all the memory
allocations that are done by Python, which are kind of tied to the CPU usage because the more objects
you're going to allocate, and when I mean allocate, I mean, even if they don't stay around, instead you
want to create a new string, a new object or whatever, even for a few seconds, milliseconds,
it costs you memory, like you have to call malloc, and there's a way you have to allocate memory,
which takes time. So you will see that. So if you use a lot of objects that are not reused,
for example, you might want to see that. The latest one we shipped two weeks ago is the
heap profiler, where you actually see a sample of your heap, like the memory you use in real time,
and what has been allocated on the heap.
Can you tell me how many of each type of object I have?
Like you've got 20 megs in lists, you've got 10 megs in strings?
No, no. I mean, in theory, yes. In practice, no. And I'm actually fighting upstream with the
CPython folks to be able to do that. It's a limitation of CPython right now. Technically,
I can't really do that, but I'm able to give you the line number of the file, the function name,
and the thread that has allocated the memory. And yeah, I wish I could know the class name,
but it would be amazing. Like for example, Java has that, and I want to have that in Python.
That will be my battle for next year.
I mean, if you have a memory leak, for example, which is quite common, right, where you keep
adding more objects on top of each other, and at some point, your memory grows forever,
and you don't know where that comes from. With such a tool, a profiler, you're able to see
which stack trace is going to add more and more and more memory forever, and you'd be able to...
I see.
It won't give you the solution to your problem. It will give you where to look at,
which usually is...
It's still pretty good.
Yeah.
Yeah, it's like 90% of the problem.
So yeah.
So can you talk a little bit about the internals of like how this works? I'm guessing it's not
using Cprofile.
No.
Directly. Is it using other open source things to sort of put the service together, or is it...
No, it's custom stuff. Everything is open source, so we want you to look at it. It's on our
DD trace repository on GitHub. So the way it works for the CPU and WorldTime Profiler, it's pretty easy.
A lot of people know about that. You can actually ask CPython to give you the list of running threads.
So if you do that 100 times per second, you get a list of running threads, and you can get the stack
trace they're running, like the function name, line number that they're running. So if you do that
a lot of time, you get a pretty good picture of what your program and threads are doing most of the time.
So, I mean, in summary, that's how it works. It's pretty easy. Then there's a few tricks to get the
CPU chime, et cetera, using the P thread API, but that's most of it. And for memory, there are actually
a good thing that has been done by a friend, Victor Stinner, which is one of the CPython.
He's done a great amount of performance improvement, like really important stuff, yeah.
And one of the things he did in Python 3.4, so it was a long time ago, is to add this module trace
malloc, which we don't use. I mean, actually built on top of it at some point, but we don't use it
anymore. We built a lightweight version of it, but it opened the memory API of CPython, where we can
actually plug your own memory allocator to CPython. And that's what we do with our profiler. We replace
a memory allocator bar, a tiny wrapper that catches every location and then do profiling on top of it.
Right, exactly. So when it says allocate this, you say record this was allocated.
Exactly.
And then allocate it, right? Something like that.
Yeah.
Is this the thing you were talking about on the screen, this dd trace py, or is it?
Yeah, exactly. You have a dd trace directory inside of the profiling directory and all the code is there.
So you can take a look at how it works internally. Yeah. I mean, the way we build it is to be able to be
easy to ship, to deploy. You don't require any extra permission. There are a lot of different
ways of doing profiling using, for example, Linux capabilities, a lot of things that are external and
not necessarily portable outside Linux. But the problem is that most of the time they require extra
permission, like being a root or anything, like using the ptrace API requires you extra permission,
which is great. I mean, great solution, maybe better technically for some point,
compared to what we do there. But they are very complicated to deploy. So that was another thing
that drives us, I think, for writing this.
All right. So a simple pip install, plug in a line or two, and off you go, right?
Exactly. I mean, it's pretty simple. And so for exporting the data, we use a pprof format from Go,
which is pretty standard. So you can actually use this profiler if you want, even if you're not a
data customer and you want to give it a try, you can actually export the data to a profile and see what
the data, you want the whole analytics that we provide and the fancy flame shaft with all their rainbow
colors. But you can use a pprof Go tool, which is pretty okay.
Oh, interesting. So you can get basically the raw data out of it just by using it directly. It's
just you guys provide the nice gathering. Yeah, exactly.
Yeah, you have to store the file yourself. Exactly. We provide the streamlining to our
backend and we provide ton of analysis. But if you are curious and want to take a look at how it works
and what you can provide, I mean, it's a good way to do it too.
All right. Awesome. I do want to dig into your profile, your scaling book, which is
what we're going to spend a lot of time on. Sure. One final question though, can I diff
profiles like from one version to another? Because one of the things that drives me crazy is
I've done a bunch of recording, I got my numbers, and then I'm going to make a change. Is it better?
Is it worse? What has gotten better? What has gotten worse? Like, is there a way to say, compare?
Yeah. That's something we are building a data log on our backend side to be able to
track your releases and tell you, well, this is going faster. This is going slower and which functions
or method are being the culprit, like of your slowness or whatever. So yeah, I mean, that's
definitely something we want to do. Yeah. And that'd be so neat because you do maybe take a whole week or
two and you sit down and you make your code fast and you get it all polished. And then over time,
it kind of degrades, right? As people add new stuff to it and they don't really necessarily do so
thinking about performance. So it'd be cool to like, okay, here's how it's degraded. And we can just focus
our energy and making this part better again. I think that'd be great. Yeah.
All right. Well, tell us about your book. I find it fascinating. I kind of gave it a bit of an
introduction, the idea of scaling it, but official title is the hacker's guide to scaling Python. And
the subtitle is build apps that scale to billions, billions of users, billions of requests, billions
of whatever, I guess. But yeah, most apps don't do this. So I think they would be, a lot of people
would be interested in hearing about it. Right. I mean, most apps don't do this, but so many apps don't
really need to do that. So that's not a problem. I wrote that book, I think four years ago now,
because I was working, like I said, on OpenStack where I would actually try to scale the things to
billions where it would be running, like it would be apps running on thousands of nodes.
Right. And maybe any individual app is not scaling to that level, but you guys basically being the
platform as a service in aggregate have a huge amount of load put on it. Right?
Exactly. Okay.
And which at that point, like when I started to write the book, a lot of people were
flying outside Python, outside the Python realm, because while Python is slow, you can't do anything
meaningful with Python, right? So slow, you have to switch to Go. And that was a lot of things I was...
You have, that's the first thing you'd have to do is if I understand is that Python is slow,
so you have to switch to Go, right? That's, I hear this all the time.
Yeah, exactly. So I saw conferences about that. In the OpenStack project, somebody rewrote one of the
software of OpenStack in Go because it was faster and was like, nope. I mean, the Python architecture you
use is slow. That's why it's the program is slow. It's not being ready to Python, like, and there's no
need to switch to Go. Like, I mean, it's not related to the language. It's that the architecture was
different. So that's what kind of motivated me at the beginning to write that book, to be able to
share everything I learned for the years before building OpenStack. I mean, part of it and learning
on what works and doesn't work as scale in Python and to stop people switching to Go for bad reasons.
There are good reasons to switch for Go for sure, but sometimes no.
Exactly. Not just because it's necessary. So, well, you know, another example of this is people
switching to Node.js because it's, it can handle more connections, right? And the reason it can handle
more connections is because it's written in an asynchronous way, a non-blocking way. And so if you
write blocking Python, it doesn't matter if you write blocking C, it's not going to take it as well as if you
write non-blocking Python, right? And so things like asyncio and those types, ASGI servers and
whatnot, can automatically sort of put you back in the game compared to those systems that the magic of,
magic in quotes, the magic of Node was they made you do that from the start. They're like, oh,
you can't call synchronous functions. So the only way you do this is you write crazy callbacks and
tell better ways, like with promises and futures, and then async and awake get into the language. But
they forced people to go down this pattern that allowed for scale. And then you can say,
oh, look, our apps scale really well. And it's just that I think a lot of times people start with
the easy way, which makes a lot of sense in Python, but that's not necessarily the scalable way. So
start one way, but as you identify these problems, you know, maybe bring in some of the ideas of your
book, right? Yeah, totally. I mean, one of the first thing I like to say about that is like,
Python is not fast or slow first. It's like, you would say like English is slow or English is fast.
It doesn't make any sense. You have people speaking English very fast over a node. It's like
Python is a long way. When CPython is slow, okay, it's not the best VM out there actually. I think it's
far from being the best VM out there, I mean, by VM and virtual machine of the language. Like if you
look at the state of the art of, I don't know, V8 for JavaScript or
or Gral or whatever for Java or the JVM itself is pretty great nowadays. And I mean, if you look at
all of that Python, I mean, CPython is already looking bad, I think. But then it has over upside,
which gives you a good thing when you use Python and a good reason to keep using Python and CPython in
the end. So I think it's a trade-off and people are not always putting the right weights at the
right place for doing that trade-off. Yeah, I agree. One trade-off might be,
oh, you could write it in, let's say Rust or something, for example, make it go faster.
But then you're giving up the ability for people to come with just a very partial understanding of
Python itself and still being really productive, right? Like people don't come to Rust and Java with
very partial understandings of it and be super productive. They just don't, right? You got to dig in
a big bite of that whole, all the computer science ideas there. Whereas like Python can start so simple
and clean. And I think that's part of the magic. But some of the, I guess some of the patterns of
that simple world don't always make sense, right? I do like that you pointed out that not everyone
needs highly scalable apps. Yeah.
Right. Because it's really cool to hear, oh, they're doing this thing with Instagram, right?
Like Instagram turned off the garbage collector and now they're getting like better memory reuse across
their web workers. And so maybe we should do that too. It's like, well, hold on now.
Yeah.
How much are you spending on infrastructure? Can you afford just 20 more dollars and not have to deal
with this ever? Right. I mean, they run their own version of CPython. That's a fork where they turn
off the garbage collector, right? Like, do you really need to go that far?
Yeah.
So I kind of put that out there, just kind of a heads up for people before they dive in. Because
kind of like design patterns, like I feel like when you learn some of this stuff, you're like,
oh, let's just put all of this into place. And then you can end up with a more complicated system
that didn't really need all those put in together at once. And maybe like, there's no app that actually
incorporates every single idea that you've mentioned here. Just they're all good ideas in their context,
but not necessarily, you know, you wouldn't order everything on a menu and put it all on one plate and
then try to eat it.
No. Right.
Especially because if you start, like, for example, the thing people do usually is like,
you have a program. Okay. It's not fast enough. Let's not say it's slow. It's not fast enough for
you. You're like, okay, I want to make it faster. So if you can, you can prioritize thing. You're like,
okay, I could run this and parallel. You go, okay, I'm going to use threads. All right. That's easy.
There's a threading API. There are the concurrents in future API in Python. It's really easy to do,
but it adds so much complexity to your program. You have to be sure it's really worth it because,
yeah, you know, you're entering the world of concurrency and when you're entering,
yeah, I mean, you have to use locks. You have to be sure about your program is not having side effects
between threads at the bad time or anything. And it adds so much complexity. It's actually very hard to
make this kind of program right and to make sure that works. And there are so many new edge cases
you're adding by adding concurrency, being threads or anything else, but you have to be sure it's worth it.
And for a lot of people out there, it's really not worth it. Like you could have a pretty simple
application with just one process or a couple of process behind a unicorn or your Usgy Walker and be
fine forever, but it doesn't make any sense to try to optimize. And like I was saying, like early
optimization, the root of all level, don't do it. Like, unless you are sure and you actually,
you know why it's slow, you know where to, to optimize, which might be a good user for
profiler or not, depending on what you're trying to optimize. But make sure that you understand
the trade-offs you are doing. I saw so many people rushing into threads or anything like that and
writing code that is invalid and I think you crash in production because of race condition,
et cetera, and they never thought about. And it takes them months, years to get things right again,
because it's very complex and writing like multi-freaded code is not something humans do very well.
So if you can't afford to not do it, don't do it.
Well, I think going back to the earlier discussion with profiling and stuff, either
in production or just with C-profiling, measure first, right? Because, and then you know so much
better what can be applied to solve that problem. Because if the slow thing is you're waiting on the
database, well, you sure don't need more threads to worry about that, right? You might consider caching.
That could be an option. You might consider indexes to make the database faster.
But you could really easily introduce lots of complexity into the system by applying the wrong fix and
not really get a whole lot better. So yeah, yeah. All right. Let's talk about scaling. I think scaling is just the definition of scaling. It's really interesting because a lot of people see here that
like, I want an app that scales or like, man, YouTube is so awesome. It scales to, you know,
a million requests a second or whatever. I want that. And then they have one, they have their app running and
they click the link and it takes three seconds or they, they run the function. It takes three seconds.
Well, if that app scaled well, it would mean it could run in three seconds for a hundred people,
as well as it could run in three seconds for one person. Like that doesn't necessarily mean faster.
So there's like this, this distinction I think people need to make between high performance,
fast code, quickly responding, and then scaling. Like it doesn't degrade as it takes on more users,
right? Maybe you want to riff on that a little bit?
Yeah. There are two dimensions, basically, which is like we were saying,
one is more users, which is more in parallel, let's say. And what is faster, like having the
page being loaded faster. So there are two different things. If you want to really optimize
one particular use case, like page being loaded or whatever, it's really a good practice. I mean,
you can't really scale that request on multiple nodes unless it's very complicated, but like to load
a page or REST API or anything like that. Usually you want to profile that part of the code to be sure.
Yeah. And that's a case where profiling locally with CProfile,
might actually be really good, right? Like one request is actually quite slow.
Yeah.
Like you could learn a lot about that, running that in a profiler and adding the horizontal
scalability stuff might actually make it a tiny bit slower for each individual request,
but allow many more of them to happen. So you got to figure out which part you're working on,
right?
Yeah. Keeping in mind, like if you see profile on your laptop is going to be different by using
CProfile on AWS or anything you run, because like your database is going to be different.
The latency is going to be different. So it's hard to reproduce the same condition on your
developer laptop that you have in production. You don't have the same system. So, I mean,
it's really a good way and it can do 80% of the job, but in some cases, it's great to have continuous
profiling on your production system. And that gives you a good way to optimize your code and to make sure
that this dimension of being faster is covered. Then the dimension of, well, let's scale to 1000 of
users and still have the three seconds load for everyone. Then that's another problem. And that's
where you actually don't need a profiler, but you need a good architecture of your program and your code
Yeah.
And be able to spawn new process, new threads, new node, new anything that can process things in parallel
for you. And decouple, like split your program in different parts and having a good architecture.
And there you can do that with Python, with any programming language, honestly, but you can do it
also that with Python, there's only to switch to any other language if you know what you're doing.
Right. It makes such an important difference there. All right. So let's go. I thought it'd be fun to
go through a couple of the chapters of your books and just your book and just talk about some of the big
ideas there. And the first, you kind of build your way up to larger systems, right? Like you start out,
we already talked about what is scaling, but the next one that you really focused on is how do I scale to
take full advantage of my current computer? Like the one I'm recording on here is my Mac mini M1.
It has four cores over there. I have my sim racing setup. It has 16 cores. Let's suppose I'm running
on that one. If I run my Python code over there and I create a bunch of threads and it says a bunch of
Python things, there's a good chance it's using one 16th of that CPU, right?
Yeah, exactly. It's it's I mean, people will start with Python. Usually it's that issue. I mean,
pretty soon where you want to run multiple, I mean, you want to run multiple threads in Paralyze,
for example, to make sure your code is faster. And then, which is a proper way. I mean, outside
Python, it's a proper way to scale. Like running a threads, I'll allow you to run another execution
thread of your program in another CPU. I mean, and threads were not used that much 20 years ago,
because all computers, every computer, are only one core, right? I mean, your personal computer,
it was a bunch of them with only one core and nobody cared about the threads. Now that everybody,
16 cores in their pockets, it's like, whoa, who should do multiple threads, right?
So exactly. Yeah. So, I mean, and that's where you started like 10 years ago,
seeing more and more people being interested in using threads in Python because, well, I mean,
I'm doing this computation and I could do it twice to go faster. So I'm spending on new threads and
and we got, except that if you do that in Python, it doesn't work very well because there's this
global interpreter lock, the gil, which actually makes sure that your Python code works nice on
multiple threads. It's about every thread running Python code, executing bytecode, they have to acquire
this lock and L2 it forever until they're finished or until they get interrupted, which means you can only
have one thread in Python running at a time on the Python VM. And the overflare have to wait or do
other stuff which are not Python related, which is what a lot of C extension like NumPy or other C
extension you may be using are doing. They're releasing the gil and doing things which are not Python,
but still doing things that are useful for you. But in that case, they're able to release the gil and
let the rest of the Python program runs. But if your program is 100% Python, you don't use any kind of C
extension, no native code or anything, then all your threads have this giant bottleneck, which is a gil,
which blocks every thread. So if you run, I think my record is like 1.6 cores with a ton of threads on the
Python program that you can't really use. I never managed to use two cores with a single Python program
and a lot of threads. It's very hard to get to that two cores being used. So when you have a 3264 cores machine that you rent
or what you use, it's a pretty waste of resources.
Yeah. So this is super interesting. And people often see this as like Python doing a threading
restriction. And what's really interesting is the gil is really about protecting the memory allocation and
cleanup. It's incrementing and decrementing that ref count. So you don't have to take a lock every time
you touch an object or assign a variable, which would make it really slow. It'd be more scalable,
but it would be slower even in the single use case, right?
I've had a lot of experiences to do that. This is actually what you have in Java. They have this
kind of monitor, I think they call it, where you have a lock per object. And it works well for them,
but that's me the details. But for Python, there have been a few experiments to do that. And yeah,
it makes the thing very, very slower, unfortunately. So it's not a good option to do to go that road.
And I mean, there have been, if you look at the history of the gil, there has been a project called
the gilectomy a few years ago to remove the gil. I mean, there have been plenty of experiments to try to
get rid of that. And the other problem is that if we ever do that at some point, it will break the
language and a lot of assumptions around the language. Because like in Python, when you add an item to a
list, it is thread safe by definition, because of the gil for sure. But if we start by saying,
well, each time you want to add an item to a list, you need to use a lock to do that.
Either we do it implicitly, but it's very slow. Or you do it explicitly as a programmer,
then it's going to be very tedious for sure. And it's not going to be compatible with the Python
we know right now, which is not a good option. So we're stuck.
Yeah. Well, there is, have you been tracking the work on HEP 554, multiple sub-interpreters?
that Eric Snow has been doing?
Yeah, a little bit.
I think that offers some really interesting opportunities there.
Yeah, I think it's a different approach. But it's a mix, like it's a trade-off between
the multi-threading and multi-processing.
Yeah, it's like a blend, like a half and half of them. Yeah.
Yeah. And, but I think it's the most promising thing we have right now, because I don't think
the gil is going to go away anytime soon, unless somebody really take a, like a giant project
and do that. But there's nobody, unfortunately, outside,
inside the Python community, like there's no company trying to sponsor any kind of effort
like that. A lot of the Python upstream stuff from what I see are run by people, you know,
willing to do that on their free time. And some are sponsored for sure, or hired by company,
but a lot of them are not. And then there are nobody like a giant big tech company trying to push
something like that forward. So it's probably what's also not helping Python. So it's the multi-interpreter
thing is probably the next best thing we'll get.
I think it is as well, because I just don't see the gil going away unless we were to say we're going to
give up reference counting. Yeah. And if you give up reference counting and then you add like a JIT and
you get like, I mean, that's a really different change. And you think of all the trouble just
changing strings from Python 2 to Python 3. Yeah.
Like this is the next level, right? It's crazy.
Okay. We're not finished yet. I still have to maintain a lot of Python 2 code, to be honest.
So I'm not ready to do Python 4 yet. So yeah.
I don't think we're ready for it either. So I think subinterpreters is interesting. And subinterpreters
are interesting because they take the ways in which you sort of do multi-processing,
which does work for scaling out, right? You kind of do message passing and each process owns its own
data structures and stuff like that. And it just says, well, you don't have to create processes
to make that happen. So it's faster basically. Yeah. And I think one of the problems with
multi-processing is also serializing the data between the different process, which is always,
I think Stack Overflow is filled with that. People are complaining about unable to pick all the data
between multiple processes, which is very tedious. So I think, I hope that having the subinterpreters
thing will solve part of that, not having to serialize everything. Also in terms of performance,
in terms of usage for sure, but also in terms of performance, you don't have to serialize
and serialize everything. Every time you want to pass something to a subprocess, it will be a very
huge failure. Yeah. So in danger of not making it all the way through the other topics. Let me ask you
just a couple other real quick questions or comments that you call out a couple of things.
One, like the CPU scaling is a problem, except for when it's not. Like sometimes it's not a problem at
all. And the example I'm thinking of, if I'm writing an API, if I'm writing a website,
any of those things, the way that you host those is you go to a server or you use a platform as a
service, which does this for you. And you, you run it in something like micro whiskey or G unicorn or
something. And what those do immediately is they say, well, we're not really going to run it in the
main process. The main process is going to look at the other ones. And we're going to create four or
eight or 10 copies of your app running all at the same time. And it will like send the requests off
to these different processes. And then all of a sudden, hey, if you have less than 10 cores,
you're scaling again. Yeah. So that's why, I mean, threads are great for things like IO,
et cetera. But if you don't really want to scale for CPU and cores, threads are not the right solution.
And it's way better to use multiple processes. So either way, I mean,
Unicorn, you will give a good solution for web apps or the alternative to like, I mean, to that,
but yeah, or a framework like Celery for doing tasks, for example, which is about out of the box to spawn
multiple processes to handle all of your tasks on multiple CPUs. And usually you won't choose, I mean,
if you don't choose any kind of ishink IO-like framework or tornado or anything like that,
where you only do have one process running one task at a time, you can spawn multiple
processes even more process than you have cores. If you have 16 cores, you can start,
I don't know, 100 processes. If you have enough memory for sure. But memory is really not a problem
unless you, depending on what you do for sure. But like for a REST API, it's really not a big problem.
You're not using gigabytes of memory per process and per regress. So yeah,
it's fine spawning a lot of unicorn workers.
So two things that I ran across that were interesting in this chapter that you covered were
futurist and Kotlin. I'm not sure how you say that second one, but can you tell people about these two
little helper library packages?
Yeah, futurist is actually, it's a tiny wrapper around Confluent.futures that you might know in Python.
The thing, it has a few things that are not there, like the ability to have statistics about your
pool of threads or your pool of anything you use process or, which is give you a pretty good idea.
A lot of applications out there, they're like, I can scale on, I don't know, 32 threads, 64.
And you have a setting usually to scale that. And you don't really know as a user or even as a
developer, how many threads you're supposed to start and to handle your workload. You're like just typing
a number randomly and see if it works or not. And having statistics around that, it's pretty useful.
There are also some features, if I remember correctly, where you can actually control the backlog.
Like usually you have a pool of threads or processes or a pool of anything trying to handle your task.
But the more you add, I mean, it can grow forever. So having the ability to control your backlog and say,
okay, I have enough tasks in the queue. No, you have to do something like, I'm not going to take any more tasks.
So that's the pattern you see a lot in queue system. Usually people, when they design a queue system,
they design the queue system with like, there's a queue, I'm going to take things out of it and process them.
And they don't think about controlling the size of the queue so the queue can grow forever, which in theory is great.
But in practice, you don't have infinite resources to start the queue and then to process it. So you want to be able to
reject works.
Talk Python to me is partially supported by our training courses.
Do you want to learn Python, but you can't bear to subscribe to yet another service?
At Talk Python Training, we hate subscriptions too. That's why our course bundle gives you full access
to the entire library of courses for one fair price. That's right. With the course bundle,
you save 70% off the full price of our courses and you own them all forever. That includes courses
published at the time of the purchase, as well as courses released within about a year of the bundle.
So stop subscribing and start learning at talkpython.fm/everything
One of the big complaints or criticisms, I guess I should say is in these async systems that they
don't provide back pressure. Yeah.
Right. A bunch of work comes into the front and it piles into asyncio, which then piles just
massively on top of the database. And then the database dies.
And there's no place further back before the dying of the database where it kind of slows down. And so
this is something that would allow you to do that for threading and multiprocessing, right?
Yeah, exactly. And which is one of the other chapters of the book, which, well, the title is Design for Failure.
And you could write an entire book on that, which is when you write your application, usually you write
something in a very optimistic way because you are in a good mood and you're like, everything's going to work, right?
Well, and you test it with small data and a few clients, right?
Exactly. And the more you scale, the more you add threads, the more you add processes,
you want to add nodes over your network. You're going to use Kubernetes to spawn
hundreds of nodes and versions of your application. And the more likely it is to fail, like somebody's
going to unplug a cable somewhere, anything can happen for sure. And you're not designing for that.
Usually you're designing in a very optimistic way because most of the time it works.
And when, when it doesn't, if you really want to go at scale, I mean, you really want to go far and
you want to work even in extreme conditions, like when the weather is really rainy, it's a lot of work.
So that's why I was saying at the beginning, like it's a trade-off to add even threads because
then you have to handle what happens when I can't start a new thread anymore because I don't know,
my system is out of, which is pretty rare nowadays. You have a lot of space for threads usually,
but if you are on a, I mean, very limited resource system or whatever, like what do you handle of that?
Yeah. And threads pre-allocate a lot of memory, like stack space and stuff.
Yeah.
Have you heard of Locus at locus.io? Have you seen this thing?
No, I don't think so.
Yeah. So speaking of back pressure and just knowing what your system can take,
this thing is super, super cool. It's a Python load testing thing that allows you to even do
distributed load. But what you do that's really interesting is you create a class in Python and
you say you give it tasks and then those tasks have URLs. And then you can say, well,
this one, I want 70% of the time, the user's going to go there and then they're going to go here. And
then I want you to kind of delay to like, they click around, like maybe every 10 seconds or so. So
randomly around that time, have them click around. And it's just a really good library for a tool for
people who are like, well, I didn't test it with enough users because I'm just me, but what is it
actually like something like this would be a really good option, I think.
Yeah, that's a good event to gather data for profiling after that's a pretty good. If you're
able to reproduce something that looks like a production.
Oh yeah. Yeah. Yeah. That's interesting. Yeah. Right. Because you want to profile a realistic
scenario. So instead of hitting with one person, you hit it with like a hundred.
Yeah. And then get the profiling results of that. Okay. That's a good idea.
Yeah. That's really the good thing with continuous profiling is what you're able to see for real
what happens. But if you know to reproduce it, that's also a valuable option.
Yeah. Okay. Very interesting. All right. So CPU scaling is interesting. And then Python around
three, four, three, five came out with this really interesting idea of asyncio and especially async and
await that makes asyncio so much better to work with. And what's interesting is that has nothing to
do with threading. Like that doesn't suffer any of the problems of almost any of the problems of like
the GIL and so on, because it's all about waiting on things. And when you're waiting,
usually you release the GIL. Yeah. So threads are a good solution for
or I/O when you can't actually use something like asyncio because let's be honest, I mean,
if you're using your own library, which was designed five years ago, it's not designed to be async at all.
So. Right. Or you're using an old ORM and the old ORM doesn't have an async version of it. And
it's either rewriting in a new one. Yeah. Or use the old non-async, right? Something like that.
Maybe threads. I don't know. Yeah. Usually, I mean, ORM, it's a good, bad example in the sense that
it's a good example technically, but usually the problem is people writing bad queries.
Yeah. I know. Better queries than an index. I'll probably solve that most of the time. Yeah.
Yeah. Exactly. But in theory, you're right. Technically, it's a good example. And yeah,
I mean, even if it looks like it's in KIO, it's magic. Cause like you were saying, it's like the
node thing that brought that back to life where it has been used and there for the last 40 years. I don't
know. Yeah. And it's like, suddenly everybody's like, well, that's amazing. And that's how you would
write anyway, any web servers for the last 30 years, but it's great. Now it's built in Python. So it's
pretty easy to use and it's a very, very well, I mean, I think it progressed a lot over the years,
like a couple of years ago where everybody was using Flask or Django, which is still true, but there's a
lot, a lot of better alternative in the sense that like Starlet, et cetera, but the past API that you
can use to build an API website based on S&KIO.
Yeah. And this whole, I think I/O world has really flourished since you wrote the book, hasn't it?
Yeah. Yeah. Yeah. When I wrote the book, I actually learned S&KIO writing the book and there was nothing.
It was very like the main plan is like, I want to use, I don't know, Redis or like we're saying a database and
there's nothing. So, or it's very low level stuff and like, it's not going to be like, yeah, I can use it, but it's going to
take me hours of work to get anything close to what I would do with the synchronous version. So
nowadays, yeah, it's totally better. I'm actually do a lot of work with S&KIO myself.
It's, I mean, everything's there for everything doing socket IO, file IO, everything is available.
There are sometimes multiple versions of the same library because don't agree on how to do Redis or
whatever, which I mean, it gives you choice. So that's, that's great. And it's a very, very good way to
not use threads. So you still have the conference program where you can have multiple tasks from
S&KIO running it, not at the same time in our space time dimension, but like being started means
pause being resume later. So you have to still take care and you actually can use lock to S&KIO.
Yeah.
But it's still a little less of a problem and issue with threads and you're not going to use more than one
CPU for sure. That's not designed to do that, but you will be able maybe more easily because you will have
less overhead than with threads to be able to use 100% of your CPU. Like we will be able to
max out your resource, your CPU resource. And then when you do that with one Python process,
well, just start a new one using salary, unicorn, whatever, or cotillion you were mentioning,
which is a good tool to do that.
Yeah.
Which is actually able to spawn multiple process and manage them for you. Because usually that's
a problem when you have a demand, you want to do a lot of work like the salary model for Q is a
pretty good example. When you have multiple workers and each worker is on thread doing things in the
background. If you're not using a framework such as salary and cotillion is a good small library to do
that when you can want a class and each class being spawned as a process basically and be managed by a
master process like you would do with whiskey or unicorn and managing the child, restarting them if they die,
etc. So that's a lot of work to do. You can certainly do that yourself, but cotillion does that for you out of the box.
Yeah. Yeah. That's a cool way to create those sub processes and stuff. But yeah, I think async.io is a
lot of promise coming along. It's really been growing. SQLAlchemy just released their 1.4 version, which
actually, so SQLAlchemy now just in like a couple of weeks ago now supports a wait session query of thing.
Not exactly that syntax, but almost they've slightly adjusted it, but pretty cool. All right.
And then one of the things that you talked about with scaling that I agree on that is super important
is statelessness. So yeah, if you want to, and I suspect going from one to two is a harder than
going from two to 10 in terms of scaling, right? Yeah. Sooners you're like, okay, this is going to be
in two places. That means it has to be stateless and it's communication. All right. All these things,
if you're just putting stuff in memory and sharing the pointers and kind of storing a persistent memory
session for somebody, well, then putting that in two places is really fraught. Yeah. It's really the
thing where I like to say that if you start using multiple processes, you're actually pretty ready to
handle multiple nodes like over a network because using multiple threads, you are always in the same
program. So it's tempting to share the state of everything between your different threads. And then
you have concurrency issue and you need lock, et cetera. But a lot of people go that road being,
I don't know, maybe a bit naive and saying, oh, that's easy. I don't have to leave my program. But if you
already, you go to the step where you actually, okay, to split your work into multiple process,
which might have to communicate between themselves for sure. And they can start by communicating over
the same host. But then you just add networks in between the process and you can scale to multiple
nodes and then over whatever number of nodes you want. Then the problem is to handle connectivity
issue that you don't have if you run on a single host between process. Usually you don't have somebody
unplugging the invisible cable. But if you're ready to handle that network failure, which will happen
for sure, between your different processes, then you can scale pretty easily on different nodes.
But as you were saying, it's like you have to switch your padding when you write your program,
which is being as stateless as possible, which is why I wrote an entire chapter on functional
programming because, well, I love functional programming. I love Lisp. And I would do Lisp if it was more
popular, but I have to do Python. So I'll do Python. It's a great Lisp. And then functional programming
gives you a pretty good way of writing code and give you a good mindset, I would say, to write code that
avoids to do side effects. And that makes your program stateless most of the time, which makes it very
easy to scale.
Right. The more statelessness you can have, the easier it is going to scale. And you can get it down to a
point where maybe the state is now stored in a Redis server that's shared between them or some
or even in a database, like a real common examples, just put it in a database. Right. So like on the
training site that I have, people come in, the only piece of state that is shared is who is logged in.
And when it comes back, it goes back to the databases. Okay. Well, who is this person? Actually,
do they have a course? Can they access this course? Like all those things are asked every time. And my first
impression of like writing code like that was like, well, if I have to query the database for
every request to get back all the information about whatever it is I care about for them tracking on
this request, it's going to be so slow, except for it's not really, it works really well, actually.
And it definitely lets you scale better. Yeah. Yeah. Yeah. It's pretty interesting. Okay. So stateless
programming, which means like functional programming, you want to just like call out that example of
remove last item that you have on the screen here, the first page of that book. Yeah. I think it
to give people a sense of what you're talking about. Yeah, exactly. It's like, I was trying
to explain in that chapter what's a pure and non-pure function where you actually have one
function doing the side effect. I mean, when you pass argument, like functional programming,
if you never know of it, it's pretty simple. Imagine that all your functional black boxes and
that you are going to put something in it. And when you get something out, you can't reuse the thing
that you put inside. Like you're going to use only what's being with put. So when you don't do a
pure function and function programming, you are going to pass a list, for example,
and you're going to modify it and not returning anything because you actually modified the list
that you passed as an argument, which is not functional at all because you actually...
Like maybe like list.sort would be an example, right? Like it...
Exactly. Yeah.
Yeah.
The thing you're calling sort on is changing the list itself. Yeah.
Yeah. Which is, yeah. And that's straight off because list.sort is really faster than sorted,
putting sorted on the list, but it's not functional. But if you call sorted or if you return the list
minus the last item or a function that removes the last item, then it's functional. You're not
returning the same list. You're writing a new list with a different output, like the last item being
removed, but it is stateless. Like you can lose the first, what you put as an input. You don't care
anymore. You have something that is outside. And if you design all your program like that,
it's pretty easy to imagine having a large input of data, putting that into a queue,
but having a worker taking that, doing whatever they need to do, and then putting something and
putting that into another queue, database, whatever you might want to do. And that's the basis of
anything that scales is due to be able to do that, to be able to scale and do a synchronous task in
the background.
Cool. Yeah. I think list.sort versus sorted of list is like the perfect comparison there.
Yeah.
All right. You touched on queues and I think queues have this possibility to just allow
incredible scale, right? Instead of every request trying to answer the question or do the task it's
meant to do entirely, all it has to do is start a task and say, "Hey, that started. Off we go." And
put it into something like Rabbit queue, Celery, Redis queue, something like that, and some other thing
that's going to pull it out of there and get to it when it gets done, right?
Yeah, exactly. It really depends on what you do and what you're trying to solve with your application,
library, whatever. But as a general thing, it's a pretty good way and architecture of a program to add that.
Like if you find a rest API, which is what people do most of the time now, I mean, you can definitely
process the request right away. And if you know that it's going to take, I don't know, less than one
second, okay, it's fine. You can do that right away. But if you know it's going to take 10 seconds, 20 seconds,
it's very, very impractical for a client to keep the connection open for 30 seconds for good and
bad reasons, but it's a problem.
Right. And almost anyone's going to think it's broken.
Yeah.
Even if it technically would have worked, you're like, "It's been 10 seconds and it's something
wrong. This is not okay, right? Like it's just not the right response."
Yeah. And I mean, connection can be cut. So if you need 20 seconds to do anything,
and then it is being cut at 18 seconds, then you lost your time and the client has to retry.
So it has to repost the same payload and then you have to reprocess it for 20 seconds. So you are
actually losing time. So it's way better to take the input, start it in a queue, reply with 200,
okay, I got the payload, I'm going to take care of it. And then I will notify you with the webhook.
I'm going to give you the result at this address, whatever mechanism you can use to do asynchronous.
But I mean, building this kind of system is a kind of slew when you have the worker taking
message from the queue, processing them, putting the results somewhere else. It's sort of a really
good way to scale your application. And you can start without, I mean, you can start with Python
itself. Like there's a queue in Python, there's multi-process and you don't have to like deploy
RabbitMQ, whatever. You can actually start if you know your program is not...
You could even just have a background thread and a list.
Yeah, exactly. I mean, you can start with something very simple for this pattern.
You don't have to use a huge framework or whatever. If you know the pattern and you
know it applies to what you're doing, you actually can use it. And you know, for example, that you
will never need more than one host, one node, one computer will be enough forever for your program.
While you don't need to deploy a network-based queue system like Redis, Rabbit or whatever.
You can use Python itself, use a multiprocess queue, and that will solve all of your problem perfectly.
Yeah, that's a great example. And the multiprocessing has an actual queue data structure that properly shares across with notifications and everything across these multiprocess processes, where the multiprocess thing can just say, I'm going to block until I get something from the queue.
And then as soon as you put it in, it picks it up and goes. But otherwise it just chills in the background.
Yeah. Very nice. All right. Moving on. Designing for failure. That's a good one. You know, the thing that comes to mind is the, at the extreme end of this, when I talked about scalability, I maybe said YouTube in a million requests a second. This one is chaos monkey and Netflix.
Yeah. You have to design for that. Like I was saying, like a lot of people, that's why you write their code with a very optimistic mindset, like everything is going to be fine. And I don't really care about error and exceptions where you actually want to write proper exceptions, like proper classes of exceptions and proper unlink of exceptions in your program.
And making sure that when you, for example, when you use, I don't know, Redis and you use a Redis library, you want to be sure to be aware. And that's something that's not very obvious, honestly, because it's honestly not really well documented. Like you can read the API of a Redis library and see, okay, it takes that type as an argument and you're going to return that type. But you don't know which exception is going to be raised. So sometimes you have to sit with your own eyes in prediction, like, oh, it's broken. It's going to raise connection error. Okay. No, I know I need to fix it.
Well, the tricky part of that is not necessarily seeing the exception and knowing it, but now what?
Yeah.
Like when I get the connection error, it means I can't talk to the database, it's overloaded or it's rebooting because it's patching. But then what happens, right? Like how do I not just go, well, there was an error. Tell the Datadog people there was an error. So we know, and then crash for the user. Like what do you do beyond that?
Yeah. And the answer is not, I mean, it's not obvious. It truly depends on what you're doing. Like if you are in a REST API and your database connection,
it's broken, you can't connect to the database. I mean, what are you going to retry for how much, I mean, how many times, for how many seconds are you going to retry?
Because the other guy is waiting on the other side of the line, right? So you can't do that for 10 seconds too long. So you have to do that a few times. Then what do you do? Do you return a 500 error and crash? Or do you return something that is like a retry later?
I mean, there's a lot of, and you have to think about all of that. Like when to say to the client to retry, if they can retry or just, yeah, crash. That's also an option sometimes. And there are so many patterns, most of the time network errors, but it might be disk full or whatever. And you have to, so you can't think about everything at the beginning for sure. So you have to have a good report system and then it's to redeploy.
I totally agree about the reporting system. It's hugely valuable and notifications as well. Because if you don't look at the reporting system, the log is full of errors and nobody would look for a week.
But are you a fan of the retry decorators? You know, I'm talking about some of those things you can say, here's like an expansional back off. I like you to retry five times and like first after like a second and then five seconds, then 10 seconds. What do you think?
I'm the author of Tenacity, which is one of the most widely used.
Yeah, okay. So that answers that question. You're a fan.
Exactly.
Okay, cool.
I am a fan. And it's all 80% of the problem. I mean, it's up to you to know how to retry. But it's a very, very good pattern to use. I mean, Tenacity provides that as a decorator, which is not the best strategy.
If you want to have different strategy or like this function should be retry this number of times depending on who the color is. But most of the time, it's good enough. Actually, it's good enough. Like most of the time, it's better to use that in a naive way where you just retry five times for five seconds or whatever, but not doing anything.
Because if you know, it's also not a silver bullet. Like I see sometimes people using it to like, well, if anything wrong happened, I'm just going to retry, which is not like, please use proper exception types, like catch the right thing and retry for the right reason, not for everything.
Right. Like maybe retry with connection timeout, but some other thing that crashes, like authorization failure, like that's never going to get better.
Exactly. Exactly. But sometimes you see people writing like, well, I'm going to retry an exception. Whatever is raised, I'm going to retry, which is really not a good idea because yeah, it's going to be fine for network errors.
But like you were saying, if it's an authentication, you don't want to retry. So I mean, be careful in that. But then if you know that you've got an IO error most of the time because the network is done one day, then it's fine to do that.
And it's a really, really good way to design easily for this kind of thing.
It doesn't serve everything. And for, I don't know, if you have a large job, for example, but you know it's going to take 10 minutes to compute, et cetera.
I mean, having there and there, this kind of retry is going to save you because in a framework like Celery, for example, if your job fails after five minutes, for whatever reason, it's just going to put it back in the queue and retry later.
But you lost the five first minutes that you used.
Yeah. And you can end up in these poison message scenarios where it tries, it fails, it goes back. It tries, it fails, it goes back. It tries, and then, yeah, then it's not so great.
All right. Just a little bit of time for some more deployment.
Yeah.
Talk about, in your book, you talk about deploying on a platform as a server, as a pass, like Heroku. There's always VMs. These days we have Docker and Kubernetes. I mean, honestly, it's not simple to know what to do as somebody who's a newcomer, I think.
Yeah. And I think it says a lot since I wrote the book, but nowadays I still, I mean, Heroku is still there and pretty widely used because it's a good solution. The thing is that deploying Python application, like for myself, I'm a pretty good Python programmer, but then outside of Python, like infrastructure and Kubernetes, I barely know anything about it.
It's like, it's a full-time job and it's not my job. It's another side of another job. So I could learn for sure. And I could be an expert in Kubernetes and deployment of anything. But I mean, it's fine to do that if you want to do that. But as a Python developer, I don't really want to do it.
I'm happy to use any kind of platform, the service like Heroku, where I can actually, like the using the Kubernetes container approach of deploying and spawning a lot of this to scale is not my responsibility, but I can outsource it to somebody that knows how to do that. So there's plenty of options. I think I wrote up on Heroku, OpenShift does that too. I know, I mean, Amazon or Microsoft or Google, all other solutions to do that too.
Yeah, they must have something. Yeah.
Yeah. And I mean, there's no reason if you really know that your application is going to scale and you don't want to spend a lot of time on infrastructure and learning. I mean, Kubernetes, Docker or whatever. I mean, you can spin easily an application on top of Heroku and then click a button to have two nodes, three nodes, four nodes, ten nodes, and then wallet. And it's expensive. That's another issue.
Yeah, the platform as a service, often they exchange complete ease of use with maybe two things, one with cost, and then the other is with flexibility, right? Like you kind of got to fit their way. Like, well, the way you use the database is our managed database service. And if you don't like that, well, then I don't know, you got to just use our managed service. You know what I mean? Things like that are kind of somewhat fixed. But yeah, I think it's really good for a lot of people.
Yeah, exactly. I mean, it covers 90% of the market, right? I mean, most people are going to start with, even if it's not a bad project, but like you're starting your company, you're doing a small project, and maybe it's one day you will be the next girl you have to scale.
But at that time, you'll solve the problem. You'll get plenty of money to solve it. But until then, you don't have a lot of time, you don't have a lot of money. It's actually pretty cheap compared to the time you would spend learning the ropes of Kubernetes. I mean, a secure deployment at scale of Kubernetes, I'm sure it's pretty more complicated than writing a simple Flask application. So it's a trade-off. I think it's a pretty good trade-off if you really want to start saying, okay, I think at some point I will need to scale. I can't run on my laptop anymore. I need to run that somewhere. While using a platform like that is a pretty good trade-off.
Yeah. And I think it's so easy to dream big and think, oh, I'm going to have to scale. So if I'm going to deploy this, what is it going to be like if I get the first 100,000 users? You should be so lucky that you have that problem, right?
Exactly.
So often things get built and they just stagnate or they don't go anywhere. Or the reason they stagnate is you're not adding features fast enough because you spent so much time building complicated architectures for a case in the future.
When reality is on the past, you could just pay 10 times as much for two weeks and then completely move to something else and you could buy yourself that time for $500, right?
But you could spend months building something, right? Like that's going to support some insane future that doesn't exist.
And so a lot of people, they'd be better off to just move forward and then evolve and realize it's not forever.
It's a path towards where you're going to be.
Yeah. And then learn marketing to have people coming onto that project.
That is the problem. Yes, that's the hard part.
Yeah. That might be my next book about like doing marketing to getting people on your project to be able to scale them.
Yeah, I'll definitely read that book if you write it.
All right. We got some more topics to cover, but we're down to just one that I think we have a little time to touch on because it's like magic sauce.
For a database, the magic sauce is indexes.
For many other things, the magic sauce is caching, right?
If this thing is really slow, I'll give you an example from TalkPylon or maybe even better from Python Bytes.
That's a more interesting example.
The RSS feed for that thing is made up out of, I think we had to limit it because we got too many, but for a while it's made up of 200 episodes.
Each episode is like five pages of markdown.
In order to render that RSS feed on demand, I've got to render, I've got to go to the database, query 200 things, and then markdownify them 200 times and then put that into an XML document and return that.
And that is not super fast.
But you know what? If I take that result, I would have returned and save it in the database and just generate it once a minute, then it's fine, right?
It's like magic. It goes from one second to one millisecond, and you just get so much scale from it.
Yeah, that's exactly what you're saying.
It's a pretty good pattern when you have to optimize.
So that would be more for the performance dimension when you want to code to be faster, not necessarily to scale the number of users.
Even if that's sometimes connected, like if you have 200 people requesting at the same time, your RSS feed and you have to do that the same time, 200 times, that's pretty useless.
So, I mean, caching is a pretty good pattern.
There's nothing actually very specific to Python there.
I mean, even in this chapter of the book, it's actually pretty, like I'm talking about how to use memcache already.
So whatever you want to cache, there are pretty good solutions to cache over the network.
You can start by caching locally in your own process, like memorizing.
Right, like a Python dictionary is a really good cache, right? For certain things.
Exactly. And there are, in Python 3 something, they added the, they were the LRU cache and they added a lot of...
Yeah, the LRU cache decorator is really neat.
Yeah. Cache tools, the cache tools library in Python.
There's a lot of different algorithms if you want to cache locally in your own Python program.
Like if you know you're going to call this method hundreds of times for...
And the result is going to be the same.
Just cache it. It's expensive to compute.
I mean, expensive to compute might be more expensive in terms of CPU, but it also might be expensive for database.
Or sometimes the expansiveness is going to be the network.
Like you're going to request some data over the network and it's very far or it's a very slow system or very unreliable system.
Yeah.
So using caching system is a pretty good solution to avoid this capability, which is also linked to the design for failure we talked about before.
Right. If you're consuming third-party services, you can't necessarily depend on their uptime, on their reliability, on their response time, all those types of things.
I'll give you another example of expensive.
So when you go to our courses, we've got, well, video servers throughout the world and we want to serve you the video from the one closest to you.
So we have a service that we call that takes your IP address, figures out where you are, and then chooses a video server for you so you get the best time.
That costs a little tiny bit of money each time, but with enough requests, it would be hundreds, maybe even, I don't know, definitely into the hundreds per month of where is this person API charges.
And so we just cache that.
Like if this IP address is from this city or this country, we just put that in our database.
And first we check the database.
Do we know where this IP address is?
No.
Go to the service.
Otherwise, just get it from the database.
It's both faster and it literally doesn't cost as much.
It's less expensive in the most direct meaning of that.
And then you're eating on the first and biggest issue in computer science, which is cache invalidation, which is what in your case, the IP might not change of country pretty often.
It can change.
Not very often, but it can change.
Yeah.
So for our, just for our example, what I did is it's in MongoDB.
So I set up a, an index that will remove it from the database after six months.
Yeah.
Which is final, but arbitrary, right?
Yes.
It's totally, I could be wrong for a while.
I mean, exactly.
But the failure case is it's slow streaming with buffering.
Potentially it's not complete failure, completely the wrong answer.
Right.
So for us, it's acceptable.
Yeah, exactly.
So you made a trade off, which is totally fine for your risk.
And that's a lot of things that you do when you want to scale is straight off.
And sometimes you don't get things totally right, but it's fine.
It's just not the best experience for your user in your case, but it's fine and you can live with it.
And I think it's a change of mindset when you go from, I'm writing a Python program, which has to be perfect and works 100% of the time.
And then when you want to scale, you have to do a lot of stuff where like, it will work fine for 80% of the people.
And for some cases, 5% of the time, while that might be not optimal, but it's fine.
And that's a lot of doing things at scale are changing this mindset to, well, it works always.
It's always true.
And no, sometimes it's not really true.
I mean, if you had a way for you to be aware and notified that an IP address changed its country, you could invalidate your cache and then make it totally reliable.
I mean, for a few seconds, maybe it won't be up to date, but that would be close to perfection.
Yeah.
But you don't have that system.
So you have to do what you did, which is a good trade-off.
I mean, it's pragmatic.
You have to be very pragmatic when you do things at scale.
Yeah.
And also kind of designed for failure.
Like what's the worst case if that goes wrong?
Yeah.
Right.
It's like streaming halfway around the world or something.
Whereas other things, like if the database goes down, you've got to deal with that entirely differently.
Yeah.
That's a hard one to fix though.
I didn't really know what to do there.
Well, I mean, caching, like you could cache, for example, if your database goes down, you can.
That's true.
You could cache older version and reply to the client.
Like it might be an older version.
I'm sorry.
Like what the time system is there.
Or I mean, depending on what you build, obviously you have to know the use case and the trade-offs
you're going to do.
But caching could be a solution.
And then the problem is that you have to.
That's a good idea because you might actually be able to say, if I'm going to go to Redis
and if it's not there, then I'm going to go to database.
Many of the requests that come in might never need to go to the database again.
You know, if you just say, oh, whoops, the database is down.
We're just going to serve what's out of the cache.
It could go for a while until there's some write operation.
As long as it's read-only, it might not matter.
Which is what services like Cloudflare does for the web, for example.
Like we do caching, protect you.
And if you're down, they're just going to show the page they had a few seconds ago until you're
back up and nobody will notice.
Yeah, interesting.
Yeah, you can apply that.
And the thing you have to keep in mind when you do caching is to be able to invalidate your
cache.
Like if you're caching database and something changed in the database, you have to have this
callback mechanism where your database can say to your cache, hey, by the way, that changed,
you need to update it if you're able to, like, you need to be aware of that.
Otherwise, you have to put arbitrary timestamp, like you said, for your, like, six months.
It's going to be six months and that's it, which is fine for such a use case.
But for a lot of people can, like, your RSS feed, but wouldn't work very well probably
if you were doing six months.
Yeah, that would be bad.
All of a sudden, oh, there's 24 new episodes all of a sudden or something.
Yeah.
So this is where you write that cron job that just restarts Redis once an hour and you'll
be fine.
No, just kidding.
You're right.
This caching validation really, really is tricky because if you check the database, then you're
not really caching anymore, right?
You might as well not have the cache.
So yeah, it's super tricky, but definitely a bit of the magic sauce.
All right.
I think that there's plenty more we could cover in the book and about scaling and architecture
and it'll be really fun, but we're way over time.
So we should probably just wrap it up with a couple of questions here that I always ask
at the end of the show.
So Julian, if you're going to write some Python code, what editor do you use?
Emacs.
I've been an Emacs user for the last 10 years.
I think I still have the commit access to Emacs itself.
Oh, cool.
You did say you love Lisp and you get to have your code powered by Lisp.
Exactly.
I stopped, but I wrote a lot of Lisp like 10 years ago.
Yeah, cool.
And then notable PyPI package, if you want, you can shout out Tenacity, which we covered
or something else if you'd like.
Tenacity and Daiquiri, which I love.
Daiquiri is a tiny wrapper around the logging system of Python.
So the story is that I never remember how to configure the logging system in Python.
Like I do import logging and I'm like, I don't know how to configure it to work like I want.
So Daiquiri does that.
It's pretty easy to use.
It has a functional approach like Tenacity in its design and it's making like two lines
to use the logging system.
You have something that works out of the box with colors, et cetera.
So I like it.
Oh, fantastic.
Yeah.
I always forget how to set it below.
I get to send me something else as well.
It's just like, I don't need to remember this.
Fantastic.
All right.
So thank you for being here.
Thank you for talking about this and covering all these ideas.
They're fascinating.
And they always require some trade-offs, right?
Like it's when should I use this thing or that thing?
But if people want to get started, one, where do they find your book?
Two, what advice in general do you have for them going down this path?
You can find my book, goingbython.com.
If you want to take a look, it's a pretty good read, I think, to give you the right mindset
to understand what the trade-offs you might need to be to do a program.
And I think that's what boils down to that.
Like, what are you ready to change and how to design your program?
And what are you going to, what is going to be a real use case?
Like, why do you want to scale?
And are you going to scale for real?
Or are you just thinking that you will need to scale in the future and do the right trade-off
and don't overcomplicate things because you're just going to shoot yourself in the foot by doing that?
Yeah, it's super tricky.
I would just add on to that.
What you think you might need to scale in the future is a web app or web API.
Use Locust to actually measure it.
And if what it is something more local, like a data science type of thing or something
computationally locally, run Cprofile with it and just measure, right?
However you go about your measuring.
Yeah.
Fantastic.
All right.
Thank you, Julian.
It's been great to chat with you and sharing these ideas.
Great book.
Thank you, Michael.
Yep.
Bye.
This has been another episode of Talk Python to Me.
Our guest in this episode was Julian Danzow, and it's been brought to you by 45 Drives and
us over at Talk Python Training.
Solve your storage challenges with hardware powered by open source.
Check out 45 Drives storage servers at talkpython.fm/45 Drives and skip the vendor lock-in
and software licensing fees.
Want to level up your Python?
We have one of the largest catalogs of Python video courses over at Talk Python.
Our content ranges from true beginners to deeply advanced topics like memory and async.
And best of all, there's not a subscription in sight.
Check it out for yourself at training.talkpython.fm.
Be sure to subscribe to the show.
Open your favorite podcast app and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, the Google Play feed at /play,
and the direct RSS feed at /rss on talkpython.fm.
We're live streaming most of our recordings these days.
If you want to be part of the show and have your comments featured on the air,
be sure to subscribe to our YouTube channel at talkpython.fm/youtube.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
I'll see you next time.
Thank you.