Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm pretty pleased with Elasticsearch's progress on durability. The snapshot/restore feature has been pretty nice to work with!

That said, having a "single source of truth" and regularly refreshing Elasticsearch is a huge pain in the butt. I currently maintain a ~500 line syncing script that takes ~15 minutes to run.

Adding a new field in a doc_type means:

- Adding a column in Postgres

- Adding a field in the doc_type mapping (I think being explicit with the field is better practice)

- Adding code to the syncer to update the field.

Ouch.

Also, the syncing scripts required a steep learning curve. At first, I was upserting everything and the syncing script took forever. To solve this, I made the syncer fetch all the documents in Postgres and all the documents in Elasticsearch then update the specific changes (thankfully, the dataset is small enough that it easily fits in memory).

I'd really love to scrap this portion of my infrastructure...



Just today I successfully finished an experiment: An existing web-app with Postgres as the source of truth pushing update events to a Kafka queue. From there, a screenful of Go forwards those events into Elasticsearch.

It doesn't solve every problem and it might be a lot of new parts if you're not going to use Kafka for anything else. I will be using it for other things like caching and push events. This kind of syncing problem seems to crop up in a lot of places.

A nice thing is that I don't have to care about Elasticsearch durability much, because I can simply rerun the ingester from the beginning of the log, as long as Kafka doesn't lose data.


I think putting a stateful, persistent, transactional stores in the middle of two stateful, persistent, transactional stores is a bad idea.

The idea is to sync secondary store B to continuously be a perfect replica of A, so: A —> B. What a lot of people do, you included, is add a third store as an intermediary: A —> Q —> B. Now you have three complex pieces of software rather than two.

The thing is, you already have the state that Q covers: It's A.

For the record, we made the same mistake. We put RabbitMQ in the middle. Now we had many problems:

* What do we do if the queue loses messages? (Manually reindex from N days ago.)

* How do you know if the queue has lost messages, due to bugs, or network downtime or similar? (Well, you don't really know. If unsure, manual complete reindex.)

* What do you do when the PostgreSQL transaction completed, but it's for some reason unable to reach RabbitMQ to post a queue message? (Manually scan logs, figure out how far back to backfill, reindex from there.) Fixing this "correctly" would require some kind of two-phase commit, which queues don't support.

Lots of manual intervention required to run a system flawlessly. It's not just about running a consistent system; it's about knowing when you're consistent or not, and how to repair.

Also, there are logistical issues:

- What do you do when people run batch jobs producing millions of updates, and users want to update documents (and see their changes) at the same time? You have no recourse but to create traffic lanes — queues and more workers.

- How to run multiple queue consumers. You have to use version constraints (update only if newVersion > oldVersion), because you will end up processing updates out of order, something which only works if the original source has a version field (ES does support "external" version numbers). Turns out a queue makes these checks happen more often because you often get multiple adjacent updates for the same objects. Kafka can de-dupe, fortunately, but RabbitMQ can't.

- How do you update multiple target stores (let's say, both ES and InfluxDB)? You have to create separate queues for each of them, so that you get true fanout. Now you have added more "middlemen" on top of the first one, more stuff that can go out of sync.

My conclusion after struggling with this for a while is that the database is the truth, but it's also the only one that's coherently transactional. So one should keep the change log close to the truth, meaning in the database. You can use a PostgreSQL "unlogged" table for performance.

Secondly, you will want to use time-based polling that invokes a simple state machine that can travel back in time. Basically, you run a worker that keeps a "cursor" pointing into the database transaction log. Let it grab big batches every N seconds. The cursor doesn't need to be transactional as long as it's reasonably persistent; if you lose the cursor, your worst case is a full reindex, but if you are unable to update the latest cursor, worst is case is just a small amount of unnecessary reindexing. This worker can live side by side with the queue-based processor, and if you shard it, you can run multiple such workers concurrently.

Everything else comes out of this logic. For example: If ElasticSearch is empty, it can detect this, and set the cursor to the beginning of time. It knows how far back the cursor is, so it knows whether it's in "full reindex" mode or "incremental mode", something it can export as a metric to a dashboard. It can also backfill, by moving the cursor back a little bit. And by using a state machine you can also put it in "incremental repair" mode, where it can use a smart algorithm (Merkle trees were mentioned by someone else recently) to detect holes in the ES index that need to be filled.

Things like building a new index now becomes a trivial, because you just start a worker instance that points to a new index, but from the same truth data store; being smart about state, it will start pulling the entire source dataset into the new index. The old worker can continue indexing the old index. Once that instance is done, you can swap the new index for the old one, then delete the old worker.

Finally, to solve the problem of real-time vs. batch updates: In addition the above worker you run a separate worker that listens to a queues. Whenever a non-batch update happens (a "batch" flag needs to be indicated in all APIs and internal processes), push the ID of the affected object on a queue, but not the object itself; rather, let the worker pull the original from the store. This way, your queue (which requires RAM/disk) stays super lean and fast. Give the worker a small time-based buffer (like 1s) so that it can coalesce multiple updates if they're happening rapidly, and use an efficient query to get multiple objects at the same time. And use versioning to avoid clobbering newer data.

Of course, the system I've outlined is probably not workable for Google or Facebook, but it will scale well and will keep things in sync better than something queue-based.


You make some good points, but there is a big architectural difference between RabbitMQ and Kafka. The solutions you point out work just as well with Kafka as they do with a unlogged PostgreSQL table, but of course there are tradeoffs for each of them. The unlogged tables has transaction guarantees, but I am not sure if I want to hit my production database with huge read loads on every reindex of a secondary data store.

I haven't really looked into it, but Botteled Water[0] looks like it can combine the good log properties of Kafka with guaranteed delivery from postgres.

[0] http://blog.confluent.io/2015/04/23/bottled-water-real-time-...


Kafka is certainly better than RabbitMQ in some respects. (In others, it's disappointing: It's practically useless if you're not running on the JVM, as clients for languages such as Go, Ruby and Node aren't up to date with the "smart" Java client. It's also clearly more low-level and designed for large installations, and less friendly to small ones.)

The problem with storing indexing state outside the database — using a queue, for example — is transactionally protecting the gap between the database and the queue. Bottled Water is cool in that it can actually bridge that gap safely, as I understand it, since PostgreSQL will keep the decoded stream until you've been able to propagate it to Kafka. On the other hand, if you have the stream, do you need Kafka? Can't you just push it directly to ElasticSearch?

For us, this issue — this and standardizing on an elegant cross-language RPC — is probably the main architectural challenge we're facing right now in our microservice development. We have tons of microservices with private data stores that need good search and also internal synchronization between microservices, which is coincidentally the exact same problem space: You have service A with its complex data model, and then a service B that wants to subscribe to updates so that it can correlate its data with that of A. It's a complicated problem that requires a simple solution.

I am not sure if I want to hit my production database with huge read loads on every reindex of a secondary data store.

Hopefully a full reindex shouldn't happen that often, though. And a full reindex would require a full scan of your production database (not the transaction log) anyway, since you don't want to keep the entire change log around forever (and can't, since the log only starts at the point when you started running this system).


> On the other hand, if you have the stream, do you need Kafka? Can't you just push it directly to ElasticSearch?

I think the separation is something very nice here. We have something like an Apache Storm topology (though we use a own Mesos based framework here) for every datastore we want to populate. If we want to add a new datastore we just have to find a library for it and can whip up a new topology. That is much more convenient than having to build support for each datastore into something central like Botteled Water and can be tweaked nicely to the specialities of the datastore.

> since you don't want to keep the entire change log around forever (and can't, since the log only starts at the point when you started running this system).

If we initialize a new Kafka topic, we push the relevant data into it once from the production database, and after that Kafka dedupe will keep it from growing too large.


The separation is nice, although I would counter that if your only primary data store is Postgres, and you want to go the logical decoding route, Postgres already has the queue: The decoded transaction log. There's no need for a queue on top of a queue. All you need now is a client that can process the log sequentially and emit each change to the appropriate data store.

Things like Kafka would be more appropriate if you have multiple producers that aren't all Postgres.


Some good points here too.

IMHO it's a huge waste having to always be re-inserting your stuff constantly.

I believe batching with ack's is the best option for reliably sync'ing data between two systems with good performance. I know a few people that work at various companies doing events and IoT where they can't drop events and they almost always end up doing batches(or micro batches, whatever you want to call it).. Would be nice if ES offered something along these lines.


There is a Bulk API for ES, which we are using for filling it, or do you mean something else?


Thank you for your detailed thoughts. You obviously have much more practical experience with this kind of system.

Many of the problems you mentioned I am aware of, and also have no workable solution yet (detecting lost messages being the biggest - Merkle-tees sounds like a very interesting approach, maybe even applied at the log-level?).

As mentioned in another reply, Kafka does support the kind of "pointer-to-log" setup you mention. Also Kafka is designed for lots of consumers, each with different characteristics. In principle, I should be able to sync something like memcache with the same information I need to sync Elasticsearch. The same holds for a websocket-server that reads from this stream and forwards new events to web-app clients. So I don't see the need for more than one "queue" yet, maybe that will show up in practice.

Also your setup would require a lot more coordination to handle updates from multiple postgres instances, if I understood correctly.

That being said, I'm still in the experimental phase with all of this, I will publish a writeup once I gain a bit more experience.


Kafka does indeed have a good design. But it doesn't solve the potential transaciton gap between your store and the queue.

For example, if you commit a transaction but you're unable to reach the Kafka queue (because you crash, you're SIGTERMed, or there's heavy load causing a network blip, or any other number of reasons), you'll lose updates. You can't very well write to Kafka before you commit, because it's not visible yet outside the transaction.

The only way is to use a transaction log in the same database, in a way that lets the log be read after the commit is done. Logical streaming would let you do this (Bottled Water [1], as someone else here mentioned, does this with Kafka) in a safe way. It's conceptually identical to storing a transaction log table, but wouldn't require as much custom code, and you'd get incremental updates for free.

[1] http://blog.confluent.io/2015/04/23/bottled-water-real-time-...


Yes, I fully recognize the problem with double-writing. I will definitely try out Bottled Water. I was also thinking about replacing Kafka with a much simpler, lower-throughput system (because we are lightyears from LinkedIn's requirements).

Two reasons why I can't just use postgres (I'd love to): 1.) Kafka (or whatever queue we settle on) will be used for logs and metrics as well, data that doesnt flow through postgres.

2.) Postgres stores the data-model of my business-domain, at the lowest, normalized level. But derived data-stores are inherently denormalized and I want to be able to use them without talking back to my source-of-truth all the time. So currently I'm passing DTOs to Kafka, just like I would to any API request. This data is not easily available at the postgres-level.

I'm not yet sure on the right abstraction level for events. It seems very natural to have them contain information that I would send to clients directly.


So what's your "source of truth"?

We have an application that might be similar. It receives analytics events from frontends. It uses (currently) RabbitMQ to distribute it to multiple "sinks", including InfluxDB, ElasticSearch and websockets; the main sink is one that stores the events as flat files (one JSON hash per line) in S3. That's what we consider our master data.


For all application-data events I consider postgres to be the ground-truth. That is somewhat unfortunate, because one can't easily place a queue in front of the database. For metrics and logs, the Kafka topic itself (which is persisted similiar to your flat files) would become the master. The use-case is pretty similiar.

Might it be feasible to have something like postgres work with an external WAL? That would solve the problem I guess, as well as leave us with a single "persistent" system.


This really is a great explanation and the strategy I have taken with several similar types of systems (not ES, but various specialized indexes that are "slaved" to a master db). In fact, it's very similar to the replication model of MySQL.

One thing you didn't mention but should be pointed out is that you have to manage clock skew carefully. If you have a single database instance, you can rely on timestamps inserted by the db server ("select now()") but if your master database is clustered, even this is not trustworthy. And of course same goes for timestamps generated by your application. Clocks are the bane of distributed systems.

My usual strategy (aside from ensuring everything is ntp synced) is to pick a plausible time period that represents an amount of clock skew that the system should never experience and subtract that from the cursor at every poll. It means you'll always reindex the last few seconds (or whatever you pick for N) worth of data, but you won't miss out on updates.


Yeah, you should generate an always-increasing sequence number in the source database.

Timestamps cannot be used to "select ... where T > ?", since at the time of polling another record may come in with the same timestamp as the newest one, and this will skip records; "where T >= ?" won't work either unless you can de-duplicate, and that requires local state.


Unfortunately you don't get monotonic sequence numbers in distributed databases.

Timestamps actually work very well for most applications, at the cost of extra reindexing proportional to how closely you can manage clock skew. Keep in mind that you are always subtracting a reasonable max skew value and getting extra records. All operations on the secondary store must be upserts.


A lot of very good experience here, thanks for sharing. Luckily for us (ok, not just luck), we are using ES to index Hadoop (both HDFS and YARN), and we are using MySQL Cluster as our source of truth. For HDFS, we create logging tables that store mutations to inodes and we have a program that transactionally periodically pulls mutation batches and applies to the ES. Failures means ES falls behind but we have no data loss. For YARN, however, we just want to index recent stats - what containers are being allocated right now to which apps. So, we use the streaming Event API for MySQL Cluster (native C++ api) that we push into ES. Failures mean lost data, but we get near instaneous updates in ES, which is crucial as it is used as part of an interactive service (monitor YARN applicaion progress).


Could you use something like the notify[0] and listen commands to help with this?

http://www.postgresql.org/docs/9.0/static/sql-notify.html


Maybe?

It would be interesting to see what a Postgres - Elasticsearch foreign data wrapper would look like...


I have a couple of fields I keep in my SQL tables... (migrateBatch,migrateStart,migrateEnd) ... migrateBatch is a UUID field that's set with the migrateStart, from there a regular process pushes to a queue, where the data is then fetched from the DB and updated in ES/Mongo/whatever... My sproc checks for updated records against the migrate/export date, and if I ever want to re-submit something, I just clear those fields in the table.

It's not the absolute fastest way to go, but it's very solid... I can usually have 500k records migrated in well under half an hour, the actual lookups for denormalized data are a bit heavy.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: