crawshaw-io-k5slfj.txt (24360B)
1 One process programming notes (with Go and SQLite) 2 3 2018 July 30 4 5 Blog-ified version of a talk I gave at [1]Go Northwest. 6 7 This content covers my recent exploration of writing internet services, iOS 8 apps, and macOS programs as an indie developer. 9 10 There are several topics here that should each have their own blog post. But as 11 I have a lot of programming to do I am going to put these notes up as is and 12 split the material out some time later. 13 14 My focus has been on how to adapt the lessons I have learned working in teams 15 at Google to a single programmer building small business work. There are many 16 great engineering practices in Silicon Valley's big companies and 17 well-capitalized VC firms, but one person does not have enough bandwidth to use 18 them all and write software. The exercise for me is: what to keep and what must 19 go. 20 21 If I have been doing it right, the technology and techniques described here 22 will sound easy. I have to fit it all in my head while having enough capacity 23 left over to write software people want. Every extra thing has great cost, 24 especially rarely touched software that comes back to bite in the middle of the 25 night six months later. 26 27 Two key technologies I have decided to use are Go and SQLite. 28 29 A brief introduction to SQLite 30 31 SQLite is an implementation of SQL. Unlike traditional database implementations 32 like PostgreSQL or MySQL, SQLite is a self-contained C library designed to be 33 embedded into programs. It has been built by D. Richard Hipp since its release 34 in 2000, and in the past 18 years other open source contributors have helped. 35 At this point it has been around most of the time I have been programming and 36 is a core part of my programming toolbox. 37 38 Hands-on with the SQLite command line tool 39 40 Rather than talk through SQLite in the abstract, let me show it to you. 41 42 A kind person on Kaggle has [2]provided a CSV file of the plays of Shakespeare. 43 Let's build an SQLite database out of it. 44 45 $ head shakespeare_data.csv 46 "Dataline","Play","PlayerLinenumber","ActSceneLine","Player","PlayerLine" 47 "1","Henry IV",,,,"ACT I" 48 "2","Henry IV",,,,"SCENE I. London. The palace." 49 "3","Henry IV",,,,"Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others" 50 "4","Henry IV","1","1.1.1","KING HENRY IV","So shaken as we are, so wan with care," 51 "5","Henry IV","1","1.1.2","KING HENRY IV","Find we a time for frighted peace to pant," 52 "6","Henry IV","1","1.1.3","KING HENRY IV","And breathe short-winded accents of new broils" 53 "7","Henry IV","1","1.1.4","KING HENRY IV","To be commenced in strands afar remote." 54 "8","Henry IV","1","1.1.5","KING HENRY IV","No more the thirsty entrance of this soil" 55 "9","Henry IV","1","1.1.6","KING HENRY IV","Shall daub her lips with her own children's blood," 56 57 First, let's use the sqlite command line tool to create a new database and 58 import the CSV. 59 60 $ sqlite3 shakespeare.db 61 sqlite> .mode csv 62 sqlite> .import shakespeare_data.csv import 63 64 Done! A couple of SELECTs will let us quickly see if it worked. 65 66 sqlite> SELECT count(*) FROM import; 67 111396 68 sqlite> SELECT * FROM import LIMIT 10; 69 1,"Henry IV","","","","ACT I" 70 2,"Henry IV","","","","SCENE I. London. The palace." 71 3,"Henry IV","","","","Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others" 72 4,"Henry IV",1,1.1.1,"KING HENRY IV","So shaken as we are, so wan with care," 73 5,"Henry IV",1,1.1.2,"KING HENRY IV","Find we a time for frighted peace to pant," 74 6,"Henry IV",1,1.1.3,"KING HENRY IV","And breathe short-winded accents of new broils" 75 7,"Henry IV",1,1.1.4,"KING HENRY IV","To be commenced in strands afar remote." 76 8,"Henry IV",1,1.1.5,"KING HENRY IV","No more the thirsty entrance of this soil" 77 9,"Henry IV",1,1.1.6,"KING HENRY IV","Shall daub her lips with her own children's blood," 78 79 Looks good! Now we can do a little cleanup. The original CSV contains a column 80 called AceSceneLine that uses dots to encode Act number, Scene number, and Line 81 number. Those would look much nicer as their own columns. 82 83 sqlite> CREATE TABLE plays (rowid INTEGER PRIMARY KEY, play, linenumber, act, scene, line, player, text); 84 sqlite> .schema 85 CREATE TABLE import (rowid primary key, play, playerlinenumber, actsceneline, player, playerline); 86 CREATE TABLE plays (rowid primary key, play, linenumber, act, scene, line, player, text); 87 sqlite> INSERT INTO plays SELECT 88 row AS rowid, 89 play, 90 playerlinenumber AS linenumber, 91 substr(actsceneline, 1, 1) AS act, 92 substr(actsceneline, 3, 1) AS scene, 93 substr(actsceneline, 5, 5) AS line, 94 player, 95 playerline AS text 96 FROM import; 97 98 (The substr above can be improved by using instr to find the '.' characters. 99 Exercise left for the reader.) 100 101 Here we used the INSERT ... SELECT syntax to build a table out of another 102 table. The ActSceneLine column was split apart using the builtin SQLite 103 function substr, which slices strings. 104 105 The result: 106 107 sqlite> SELECT * FROM plays LIMIT 10; 108 1,"Henry IV","","","","","","ACT I" 109 2,"Henry IV","","","","","","SCENE I. London. The palace." 110 3,"Henry IV","","","","","","Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others" 111 4,"Henry IV",1,1,1,1,"KING HENRY IV","So shaken as we are, so wan with care," 112 5,"Henry IV",1,1,1,2,"KING HENRY IV","Find we a time for frighted peace to pant," 113 6,"Henry IV",1,1,1,3,"KING HENRY IV","And breathe short-winded accents of new broils" 114 7,"Henry IV",1,1,1,4,"KING HENRY IV","To be commenced in strands afar remote." 115 8,"Henry IV",1,1,1,5,"KING HENRY IV","No more the thirsty entrance of this soil" 116 9,"Henry IV",1,1,1,6,"KING HENRY IV","Shall daub her lips with her own children's blood," 117 118 Now we have our data, let us search for something: 119 120 sqlite> SELECT * FROM plays WHERE text LIKE "whether tis nobler%"; 121 sqlite> 122 123 That did not work. Hamlet definitely says that, but perhaps the text formatting 124 is slightly off. SQLite to the rescue. It ships with a Full Text Search 125 extension compiled in. Let us index all of Shakespeare with FTS5: 126 127 sqlite> CREATE VIRTUAL TABLE playsearch USING fts5(playsrowid, text); 128 sqlite> INSERT INTO playsearch SELECT rowid, text FROM plays; 129 130 Now we can search for our soliloquy: 131 132 sqlite> SELECT rowid, text FROM playsearch WHERE text MATCH "whether tis nobler"; 133 34232|Whether 'tis nobler in the mind to suffer 134 135 Success! The act and scene can be acquired by joining with our original table. 136 137 sqlite> SELECT play, act, scene, line, player, plays.text 138 FROM playsearch 139 INNER JOIN plays ON playsearch.playsrowid = plays.rowid 140 WHERE playsearch.text MATCH "whether tis nobler"; 141 Hamlet|3|1|65|HAMLET|Whether 'tis nobler in the mind to suffer 142 143 Let's clean up. 144 145 sqlite> DROP TABLE import; 146 sqlite> VACUUM; 147 148 Finally, what does all of this look like on the file system? 149 150 $ ls -l 151 -rwxr-xr-x@ 1 crawshaw staff 10188854 Apr 27 2017 shakespeare_data.csv 152 -rw-r--r-- 1 crawshaw staff 22286336 Jul 25 22:05 shakespeare.db 153 154 There you have it. The SQLite database contains two full copies of the plays of 155 Shakespeare, one with a full text search index, and stores both of them in 156 about twice the space it takes the original CSV file to store one. Not bad. 157 158 That should give you a feel for the i-t-e of SQLite. 159 160 And scene. 161 162 Using SQLite from Go 163 164 The standard database/sql 165 166 There are a number of cgo-based [3]database/sql drivers available for SQLite. 167 The most popular one appears to be [4]github.com/mattn/go-sqlite3. It gets the 168 job done and is probably what you want. 169 170 Using the database/sql package it is straightforward to open an SQLite database 171 and execute SQL statements on it. For example, we can run the FTS query from 172 earlier using this Go code: 173 174 package main 175 176 import ( 177 "database/sql" 178 "fmt" 179 "log" 180 181 _ "github.com/mattn/go-sqlite3" 182 ) 183 184 func main() { 185 db, err := sql.Open("sqlite3", "shakespeare.db") 186 if err != nil { 187 log.Fatal(err) 188 } 189 defer db.Close() 190 stmt, err := db.Prepare(` 191 SELECT play, act, scene, plays.text 192 FROM playsearch 193 INNER JOIN plays ON playsearch.playrowid = plays.rowid 194 WHERE playsearch.text MATCH ?;`) 195 if err != nil { 196 log.Fatal(err) 197 } 198 var play, text string 199 var act, scene int 200 err = stmt.QueryRow("whether tis nobler").Scan(&play, &act, &scene, &text) 201 if err != nil { 202 log.Fatal(err) 203 } 204 fmt.Printf("%s %d:%d: %q\n", play, act, scene, text) 205 } 206 207 Executing it yields: 208 209 Hamlet 3:1 "Whether 'tis nobler in the mind to suffer" 210 211 A low-level wrapper: crawshaw.io/sqlite 212 213 Just as SQLite steps beyond the basics of SELECT, INSERT, UPDATE, DELETE with 214 full-text search, it has several other interesting features and extensions that 215 cannot be accessed by SQL statements alone. These need specialized interfaces, 216 and many of the interfaces are not supported by any of the existing drivers. 217 218 So I wrote my own. You can get it from [5]crawshaw.io/sqlite. In particular, it 219 supports the streaming blob interface, the [6]session extension, and implements 220 the necessary sqlite_unlock_notify machinery to make good use of the [7]shared 221 cache for connection pools. I am going to cover these features through two use 222 case studies: the client and the cloud. 223 224 cgo 225 226 All of these approaches rely on cgo for integrating C into Go. This is 227 straightforward to do, but adds some operational complexity. Building a Go 228 program using SQLite requires a C compiler for the target. 229 230 In practice, this means if you develop on macOS you need to install a 231 cross-compiler for linux. 232 233 Typical concerns about the impact on software quality of adding C code to Go do 234 not apply to SQLite as it has an extraordinary degree of testing. The quality 235 of the code is exceptional. 236 237 Go and SQLite for the client 238 239 I am building an [8]iOS app, with almost all the code written in Go and the UI 240 provided by a web view. This app has a full copy of the user data, it is not a 241 thin view onto an internet server. This means storing a large amount of local, 242 structured data, on-device full text searching, background tasks working on the 243 database in a way that does not disrupt the UI, and syncing DB changes to a 244 backup in the cloud. 245 246 That is a lot of moving parts for a client. More than I want to write in 247 JavaScript, and more than I want to write in Swift and then have to promptly 248 rewrite if I ever manage to build an Android app. More importantly, the server 249 is in Go, and I am one independent developer. It is absolutely vital I reduce 250 the number of moving pieces in my development environment to the smallest 251 possible number. Hence the effort to build (the big bits) of a client using the 252 exact same technology as my server. 253 254 The Session extension 255 256 The session extension lets you start a session on an SQLite connection. All 257 changes made to the database through that connection are bundled into a 258 patchset blob. The extension also provides method for applying the generated 259 patchset to a table. 260 261 func (conn *Conn) CreateSession(db string) (*Session, error) 262 263 func (s *Session) Changeset(w io.Writer) error 264 265 func (conn *Conn) ChangesetApply( 266 r io.Reader, 267 filterFn func(tableName string) bool, 268 conflictFn func(ConflictType, ChangesetIter) ConflictAction, 269 ) error 270 271 This can be used to build a very simple client-sync system. Collect the changes 272 made in a client, periodically bundle them up into a changeset and upload it to 273 the server where it is applied to a backup copy of the database. If another 274 client changes the database then the server advertises it to the client, who 275 downloads a changeset and applies it. 276 277 This requires a bit of care in the database design. The reason I kept the FTS 278 table separate in the Shakespeare example is I keep my FTS tables in a separate 279 attached database (which in SQLite, means a different file). The cloud backup 280 database never generates the FTS tables, the client is free to generate the 281 tables in a background thread and they can lag behind data backups. 282 283 Another point of care is minimizing conflicts. The biggest one is AUTOINCREMENT 284 keys. By default the primary key of a rowid table is incremented, which means 285 if you have multiple clients generating rowids you will see lots of conflicts. 286 287 I have been trialing two different solutions. The first is having each client 288 register a rowid range with the server and only allocate from its own range. It 289 works. The second is randomly generating int64 values, and relying on the low 290 collision rate. So far it works too. Both strategies have risks, and I haven't 291 decided which is better. 292 293 In practice, I have found I have to limit DB updates to a single connection to 294 keep changeset quality high. (A changeset does not see changes made on other 295 connections.) To do this I maintain a read-only pool of connections and a 296 single guarded read-write connection in a pool of 1. The code only grabs the 297 read-write connection when it needs it, and the read-only connections are 298 enforced by the read-only bit on the SQLite connection. 299 300 Nested Transactions 301 302 The database/sql driver encourages the use of SQL transactions with its Tx 303 type, but this does not appear to play well with nested transactions. This is a 304 concept implemented by SAVEPOINT / RELEASE in SQL, and it makes for 305 surprisingly composable code. 306 307 If a function needs to make multiple statements in a transaction, it can open 308 with a SAVEPOINT, then defer a call to RELEASE if the function produces no Go 309 return error, or if it does instead call ROLLBACK and return the error. 310 311 func f(conn *sqlite.Conn) (err error) { 312 conn...SAVEPOINT 313 defer func() { 314 if err == nil { 315 conn...RELEASE 316 } else { 317 conn...ROLLBACK 318 } 319 }() 320 } 321 322 Now if this transactional function f needs to call another transactional 323 function g, then g can use exactly the same strategy and f can call it in a 324 very traditional Go way: 325 326 if err := g(conn); err != nil { 327 return err // all changes in f will be rolled back by the defer 328 } 329 330 The function g is also perfectly safe to use in its own right, as it has its 331 own transaction. 332 333 I have been using this SAVEPOINT + defer RELEASE or return an error semantics 334 for several months now and find it invaluable. It makes it easy to safely wrap 335 code in SQL transactions. 336 337 The example above however is a bit bulky, and there are some edge cases that 338 need to be handled. (For example, if the RELEASE fails, then an error needs to 339 be returned.) So I have wrapped this up in a utility: 340 341 func f(conn *sqlite.Conn) (err error) { 342 defer sqlitex.Save(conn)(&err) 343 344 // Code is transactional and can be stacked 345 // with other functions that call sqlitex.Save. 346 } 347 348 The first time you see sqlitex.Save in action it can be a little off-putting, 349 at least it was for me when I first created it. But I quickly got used to it, 350 and it does a lot of heavy lifting. The first call to sqlitex.Save opens a 351 SAVEPOINT on the conn and returns a closure that either RELEASEs or ROLLBACKs 352 depending on the value of err, and sets err if necessary. 353 354 Go and SQLite in the cloud 355 356 I have spent several months now redesigning services I have encountered before 357 and designing services for problems I would like to work on going forward. The 358 process has led me to a general design that works for many problems and I quite 359 enjoy building. 360 361 It can be summarized as 1 VM, 1 Zone, 1 process programming. 362 363 If this sounds ridiculously simplistic to you, I think that's good! It is 364 simple. It does not meet all sorts of requirements that we would like our 365 modern fancy cloud services to meet. It is not "serverless", which means when a 366 service is extremely small it does not run for free, and when a service grows 367 it does not automatically scale. Indeed, there is an explicit scaling limit. 368 Right now the best server you can get from Amazon is roughly: 369 370 • 128 CPU threads at ~4GHz 371 • 4TB RAM 372 • 25 Gbit ethernet 373 • 10 Gbps NAS 374 • hours of yearly downtime 375 376 That is a huge potential downside of of one process programming. However, I 377 claim that is a livable limit. 378 379 I claim typical services do not hit this scaling limit. 380 381 If you are building a small business, most products can grow and become 382 profitable well under this limit for years. When you see the limit approaching 383 in the next year or two, you have a business with revenue to hire more than one 384 engineer, and the new team can, in the face of radically changing business 385 requirements, rewrite the service. 386 387 Reaching this limit is a good problem to have because when it comes you will 388 have plenty of time to deal with it and the human resources you need to solve 389 it well. 390 391 Early in the life of a small business you don't, and every hour you spend 392 trying to work beyond this scaling limit is an hour that would have been better 393 spent talking to your customers about their needs. 394 395 The principle at work here is: 396 397 Don't use N computers when 1 will do. 398 399 To go into a bit more technical detail, 400 401 I run a single VM on AWS, in a single availability zone. The VM has three EBS 402 volumes (this is Amazon name for NAS). The first holds the OS, logs, temporary 403 files, and any ephemeral SQLite databases that are generated from the main 404 databases, e.g. FTS tables. The second the primary SQLite database for the main 405 service. The third holds the customer sync SQLite databases. 406 407 The system is configured to periodically snapshot the system EBS volume and the 408 customer EBS volumes to S3, the Amazon geo-redundant blob store. This is a 409 relatively cheap operation that can be scripted, because only blocks that 410 change are copied. 411 412 The main EBS volume is backed up to S3 very regularly, by custom code that 413 flushes the WAL cache. I'll explain that in a bit. 414 415 The service is a single Go binary running on this VM. The machine has plenty of 416 extra RAM that is used by linux's disk cache. (And that can be used by a second 417 copy of the service spinning up for low down-time replacement.) 418 419 The result of this is a service that has at most tens of hours of downtime a 420 year, about as much change of suffering block loss as a physical computer with 421 a RAID5 array, and active offsite backups being made every few minutes to a 422 distributed system that is built and maintained by a large team. 423 424 This system is astonishingly simple. I shell into one machine. It is a linux 425 machine. I have a deploy script for the service that is ten lines long. Almost 426 all of my performance work is done with pprof. 427 428 On a medium sized VM I can clock 5-6 thousand concurrent requests with only a 429 few hours of performance tuning. On the largest machine AWS has, tens of 430 thousands. 431 432 Now to talk a little more about the particulars of the stack: 433 434 Shared cache and WAL 435 436 To make the server extremely concurrent there are two important SQLite features 437 I use. The first is the shared cache, which lets me allocate one large pool of 438 memory to the database page cache and many concurrent connections can use it 439 simultaneously. This requires some support in the driver for 440 sqlite_unlock_notify so user code doesn't need to deal with locking events, but 441 that is transparent to end user code. 442 443 The second is the Write Ahead Log. This is a mode SQLite can be knocked into at 444 the beginning of connection which changes the way it writes transactions to 445 disk. Instead of locking the database and making modifications along with a 446 rollback journal, it appends the new change to a separate file. This allows 447 readers to work concurrently with the writer. The WAL has to be flushed 448 periodically by SQLite, which involves locking the database and writing the 449 changes from it. There are default settings for doing this. 450 451 I override these and execute WAL flushes manually from a package that, when it 452 is done, also triggers an S3 snapshot. This package is called reallyfsync, and 453 if I can work out how to test it properly I will make it open source. 454 455 Incremental Blob API 456 457 Another smaller, but important to my particular server feature, is SQLite's [9] 458 incremental blob API. This allows a field of bytes to be read and written in 459 the DB without storing all the bytes in memory simultaneously, which matters 460 when it is possible for each request to be working with hundreds of megabytes, 461 but you want tens of thousands of potential concurrent requests. 462 463 This is one of the places where the driver deviates from being a close-to-cgo 464 wrapper to be more [10]Go-like: 465 466 type Blob 467 func (blob *Blob) Close() error 468 func (blob *Blob) Read(p []byte) (n int, err error) 469 func (blob *Blob) ReadAt(p []byte, off int64) (n int, err error) 470 func (blob *Blob) Seek(offset int64, whence int) (int64, error) 471 func (blob *Blob) Size() int64 472 func (blob *Blob) Write(p []byte) (n int, err error) 473 func (blob *Blob) WriteAt(p []byte, off int64) (n int, err error) 474 475 This looks a lot like a file, and indeed can be used like a file, with one 476 caveat: the size of a blob is set when it is created. (As such, I still find 477 temporary files to be useful.) 478 479 Designing with one process programming 480 481 I start with: Do you really need N computers? 482 483 Some problems really do. For example, you cannot build a low-latency index of 484 the public internet with only 4TB of RAM. You need a lot more. These problems 485 are great fun, and we like to talk a lot about them, but they are a relatively 486 small amount of all the code written. So far all the projects I have been 487 developing post-Google fit on 1 computer. 488 489 There are also more common sub-problems that are hard to solve with one 490 computer. If you have a global customer base and need low-latency to your 491 server, the speed of light gets in the way. But many of these problems can be 492 solved with relatively straightforward CDN products. 493 494 Another great solution to the speed of light is geo-sharding. Have complete and 495 independent copies of your service in multiple datacenters, move your user's 496 data to the service near them. This can be as easy as having one small global 497 redirect database (maybe SQLite on geo-redundant NFS!) redirecting the user to 498 a specific DNS name like {us-east, us-west}.mservice.com. 499 500 Most problems do fit in one computer, up to a point. Spend some time 501 determining where that point is. If it is years away there is a good chance one 502 computer will do. 503 504 Indie dev techniques for the corporate programmer 505 506 Even if you do not write code in this particular technology stack and you are 507 not an independent developer, there is value here. Use the one big VM, one 508 zone, one process Go, SQLite, and snapshot backup stack as a hypothetical tool 509 to test your designs. 510 511 So add a hypothetical step to your design process: If you solved your problem 512 on this stack with one computers, how far could you get? How many customers 513 could you support? At what size would you need to rewrite your software? 514 515 If this indie mini stack would last your business years, you might want to 516 consider delaying the adoption of modern cloud software. 517 518 If you are a programmer at a well-capitalized company, you may also want to 519 consider what development looks like for small internal or experimental 520 projects. Do your coworkers have to use large complex distributed systems for 521 policy reasons? Many of these projects will never need to scale beyond one 522 computer, or if they do they will need a rewrite to deal with shifting 523 requirements. In which case, find a way to make an indie stack, linux VMs with 524 a file system, available for prototyping and experimentation. 525 526 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 527 [11]Index 528 [12]github.com/crawshaw 529 [13]twitter.com/davidcrawshaw 530 [email protected] 531 532 References: 533 534 [1] https://gonorthwest.io/ 535 [2] https://www.kaggle.com/kingburrito666/shakespeare-plays 536 [3] https://golang.org/pkg/database/sql 537 [4] https://github.com/mattn/go-sqlite3 538 [5] https://crawshaw.io/sqlite 539 [6] https://www.sqlite.org/sessionintro.html 540 [7] https://www.sqlite.org/sharedcache.html 541 [8] https://www.posticulous.com/ 542 [9] https://www.sqlite.org/c3ref/blob_open.html 543 [10] https://godoc.org/crawshaw.io/sqlite#Blob 544 [11] https://crawshaw.io/ 545 [12] https://github.com/crawshaw 546 [13] https://twitter.com/davidcrawshaw