Django under the Hood - Channels
Speaker: Andrew Godwin is known mostly for Django migrations. He works for eventbrite, and started Channels.
Links: Channels docs and Channels examples
The Problem
- There are now WebSockets, and other things that are not just Request/Response. Channels tries to solve that, since Django is (as is) not exactly suited to asynchronous work.
- Since Python is synchronous (traditionally), Django is, too.
- Synchronous code is much easier to write and debug and understand and reason about.
- Single-process async is not enough, distributed systems rock!
- To offset the problems with reasoning about async needs to be solved by proven design patterns.
- Needs to be easy to reason about, easy to understand, and easy to explain
Design Pattern: Loose Coupling via Message Bus
- Do not be tied too much to other elements: to Django, to WebSockets.
- Implemented: HTTP, WebSocket
- Drafted: IRC, Email, Slack
- Do not do: Minecraft, Mainframe Terminal
- Well-defined, minimal interfaces that can be used in all places
- Very proven: Message Bus
- ASGI: specification of how to talk to the message bus, defining five interface methods
- nonblocking send
- blocking receive
- add to group
- discard from group
- send to group
- Messages are: JSON-compatible, dictionary-based messages onto named channels
Implementation overview
- WebSocket: upgrade from HTTP
- Connect and ask for upgrade
- Server either accepts or rejects
- Send and receive whenever you want, no ordering
- Disconnect
- Server handles sockets separately from Django project
- Structure: Shared bus layer, receiving incoming, and forwarding to Django or Channels
- Mark channels that need to go to a specific group
- Works as shared-nothing: workers don't know about each other, hence the reply_channel
- Message Bus tradeoffs
- At-most-once: do not guarantee delivery
- FIFO: messages are handled ordered
- Backpressure via capacity: reject connections over capacity
- Not sticky: you might go to different servers on every message
- Ordering via an order key on receive messages, per-group
- Changed to explicit connecting for a clean start for ordering
Parts
- Channels: Django integration of ASGI
- asgi-redis: for shared backends
- asgi-ipc: local memory backend
- asgiref: common code
- Daphne: using twisted (etc), handle connections and negotiations, can handle either everything or only WebSockets
Implementation details
- Consumers
- Consumers look like views
- Interfaces againt message instead of request
- Responses are not a return; responses can be sent at any time in the consumer
- Also class-based consumers available
- Routing
- Like Django: regex-based matches
- Sessions: Channels Session:
- Contain all the relevant meta information (eg ordering)
- Generate sessions from a message's reply channel name
- uses
ConsumeLater
exception to postpone delivering a message
- No middleware
- Middleware is very request/response
- New middleware half works
- Decorators are more discoverable and explicit than middleware
- View/HTTP Django is still there
- Django as is: is now a single consumer, the ViewConsumer, inside Channels
- Send a message, receive response, send response
- Means you can drop down to Django views
- Signals and commands
- Runserver works (overridden to include Daphne)
- New signals for lifecycle handling
- Staticfiles configured for development are injected
Future of Channels
- Generalised async communication (think CSP)
- Might work on a diverse set of async stuff
- MQTT
- Microservice communication
- Separate things by CPU
- Mix Python versions
- Mix async and sync
- Diversity of implementations
- More web servers (at least a second one to Daphne)
- More channel layers
- More optimization
- Better bulk sends
- Less network traffic on receive
- Channels was right first, will be fast second
- More maintainers
- More viewpoints
- More time
- 1.0 is coming really soon
Q&A
- Use cases
- Long polling
- Live data
- Might implement sock.js in the future.
- Getting involved
- A getting started
- Concepts documentation
- Channels examples
- Further docs
- Why do URLs start with a slash?
- This is not URL routing, it's message routing.
- Is this applicable to the current Django DEP on routing?
- Yup. There ought to be a "start with a slash" DEP.
- Can you make sure that some requests are only handled by some systems?
- Yup, you do that in the runworker command. You specify which requests should be handled.
- Has HTTP over channels been load tested?
- Came out a bit slower than gunicorn, doesn't handle many (>1k) connections well yet.
- What are our profiling tools here?
- HTTP benchmarking tools, there is one for WebSockets … other than that, nothing.
- Within the spec, could there be an "at least once" mode?
- You could do it, but all consumers and decorators would need to be rewritten, dealing with duplicates - where would we dedup? It's certainly possible, but it's a lot of work, not exactly feasible.
- So many choices for transport layers, and frontend stuff - what do you recommend as default stack?
- Personally, Daphne for WebSockets, WSGI for HTTP, route via nginx. Once we reach 1.0, we should be able to use Daphne for everything.