3 ms·
Layers are not as cleanly isolated as we like to think. Take buffered IO as an example. With unbuffered IO with a dumb drive, a lot of programs run slower. H
by phoehne 4y ago
Layers are not as cleanly isolated as we like to think. Take buffered IO as an example. With unbuffered IO with a dumb drive, a lot of programs run slower. However, what unbuffered IO does is give you immediate feedback about the completion of the IO. I said write 64 bytes. It either writes 64 bytes, or fails.
When I add buffering, I now have a new possible failure state where my request to write data completed, but the data is sitting in the buffer. If the data were really important (say a database) I add a write barrier like a 'sync' call. It's one more thing I need to be aware of. Bufferring adds more ways to fail.
Then we add an additional twist. Our disk controller is really clever and has its own local cache so I can write 64 bytes, send it to the controller, have the controller say "we're good", but not have to wait for the data to be written to the platter. With the dumb drive, that wasn't the case. I had immediate feedback. But now we have a new failure state and I have to account for that as well. Maybe I have to put battery backup on the controller. Maybe I need a new primitive to really flush the data. And I might also have new errors where the controller accepts the data, attempts to write it to disk, but the disk is dying and fails to write. So my program lost data and the only way I know about it is to look in an unrelated log for drive failures. Unfortunately no one thought to check or change the card's battery backup in 3 years...
This isn't just a caching problem. Let's say you take some functionality and move it from a library to a service on the network. Now you have to account for all sorts of other issues related to network failures that just weren't part of the base case for local resources. I can no longer just make a call, I have to think about things like retries, or what I need to do if the service is unavailable. Possibly, I need to percolate up new errors to drive my code into behaviors like caching the data locally until I can send it. My debugging also gets more complicated, like 1) is the server alive (ping is often blocked so we use Test-NetConnection to do an NS lookup, ping, and TCP connection test), 2) Is the address routable? 3) Is my network connection still valid (e.g. did my local switch meet its maker)? 4) Do I have correct certificates (e.g. we can't complete the SSL handshake)? 5) Was there a protocol change (e.g. Team A is sending {"foo": 1} but Team B is expecting {"foo": "1"}, and so on.
It isn't a layering thing like well constructed, SOLID code, it's changing the assumptions and conditions you have for all sorts of states you could be in. It's even further compounded with complex services where service A depends on B, C, and D, and D is failing. But now the data in B and C don't reflect what's in D, which failed to update. Your code becomes much more complex to handle these additional failure scenarios. Your search space for possible failures is orders of magnitude larger.
- ip26 4y agoYou’re completely right, of course- in my thread of reasoning I argue that the buffered disk controller you’ve identified is a leaky abstraction layer. Battery backup and RAID are forms of hardening that abstraction. In practice, we’ve allowed that particular layer to leak persistently because it’s ultimately cheaper to handle the leaks than to harden the abstraction.