This is the final entry in a three-part series on Bitsight’s new Event Store. In the first and second posts, we described some key components of the architecture. Because of the limited number of access patterns we had to support (bulk inserts, mostly in chronological order; full scans, coarsely filtered by key range and time), we were able to implement a simple NoSQL-style database, using flat Parquet files on Amazon’s S3 as the storage layer.
Last month, we shipped the first version of the Event Store, and it’s been running in production since then. So far, we’ve been very happy with the results. In this post, we’ll relate a few of our experiences and lessons learned during the development process.
Many of the difficulties we faced were due to S3’s unique characteristics:
Slow S3 Lists
Each of our Parquet files is stored as a separate S3 object, and there are hundreds of thousands of these. We quickly learned that naively querying S3 to find our objects took a huge amount of time (especially using Hadoop’s built-in FileSystems). In fact, early versions of our code spent more time listing files and getting file metadata than in actually processing the data.
We now keep track of all of the objects and metadata ourselves, in flat manifest files. This has the additional benefit of ensuring data integrity: we notice when files are missing (due to eventual consistency, or otherwise), and we can either wait or repair the damage.
Connection Management
Adjusting our code and configuration for efficient, reliable writes to and reads from S3 required some trial and error (as official tuning guidance is sparse).
For example, an early version of our sort-merge join algorithm (for aggregating and deduplicating records upon insertion into the store) simultaneously kept open a large number of “file” handles to S3, which resulted in sporadic, hard-to-debug connection timeouts and connection pool exhaustion. We reworked the algorithm to open files only when absolutely necessary, and to close them aggressively.
Downstream, we use Hadoop’s s3a filesystem in client mapreduce jobs that ingest the Parquet files from the store. Initially, we saw an alarming fraction of tasks fail on connection timeouts and broken streams. Increasing the maximum number of connection threads (fs.s3a.threads.max) and thread lifetime (fs.s3a.threads.keepalivetime) eliminated these problems.
S3 bandwidth to EC2
One of our main concerns was whether we would see a performance decrease because we were reading data from S3, rather than local HDFS (which violates one of Hadoop’s original core principles), and we spent a lot of time testing S3’s bandwidth and latency. Happily, in the end, we actually observed a slight performance increase. Our workload is largely CPU-bound, and pulling data from S3 meant that the HBase regionservers were no longer competing for CPU time.