使用事件溯源提高了Elasticsearch的效能 - luis-sena

banq發表於2021-11-29

Elasticseach 並不真正支援更新。在 Elasticsearch 中,更新總是意味著刪除+建立,持續不斷的文件更新可能會使 Elasticsearch 叢集癱瘓。幸運的是,有辦法避免這種情況。

最終的解決方案包括使用事件溯源設計模式將所有需要儲存的更改轉換為事件。此案例的應用程式狀態將儲存在 Elasticsearch 中。

解決方案的第二部分是我們如何儲存該狀態。

我們需要將對每個文件的所有更改組合到一個操作中。這意味著在某些情況下有一個值列表,而在其他情況下只保留最後一次更改(如使用者名稱更改)。為了確保狀態一致,我們需要保證針對同一文件的事件的順序,否則我們可能會面臨狀態與我們的真實來源不匹配的風險。

該解決方案基於流的概念。我使用的是 Kafka,但 Redis Streams 或 AWS Kinesis 都可以正常工作。

這個想法是將所有新更改(如新關注者、使用者名稱更改等)儲存在分割槽主題中。確保您的分割槽鍵與文件 id 一致以保證順序,但也要避免每個使用者 id 一個分割槽,否則您將殺死 Kafka 叢集。

順序對於覆蓋最後一個欄位值(如使用者名稱更改)的事件很重要。我們要確保我們堅持使用最後一個版本而不是中間版本。

為了處理這些訊息,我們需要一個流處理解決方案。對於此示例,我將使用Faust,但它是如此簡單的模式,我建議您使用更適合您的模式。

import base64
import os
import random

import faust


# Models describe how messages are serialized:
# {"user_id": "3fae...", username": "my_new_username"}
class UserEvent(faust.Record):
    user_id: str
    username: str
    follower_id: str
    following_id: str


app = faust.App('es_event_processor', broker='kafka://broker')
topic = app.topic(
    'user_events',
    key_type=str,
    value_type=UserEvent
)


def add_value(user_docs, user_id, key, value, op):
    if user_id not in user_docs:
        user_docs[user_id] = {}
    if op == 'set':
        user_docs[user_id][key] = value
    elif op == 'append':
        if not user_docs[user_id][key]:
            user_docs[user_id][key] = []
        user_docs[user_id][key].append(value)


@app.agent(topic)
async def user_event_consumer(user_events):
    """
    Very simple way to aggregate user events and storing them into ES
    Other options like aggregating into another kafka topic
    or using RocksDB table is also valid
    :param user_events:
    :return:
    """
    print("starting agent")
    # get 1000 messages with 30s timeout
    async for lst in user_events.take(1000, within=30.0):
        user_docs = {}
        for user_event in lst:
            if user_event.username:
                add_value(user_docs, user_event.user_id, 'username', user_event.username, 'set')
            if user_event.follower_id:
                add_value(user_docs, user_event.user_id, 'follower_id', user_event.follower_id, 'set')
            if user_event.following_id:
                add_value(user_docs, user_event.user_id, 'following_id', user_event.following_id, 'set')

        # ES Load logic goes here
        # user_docs already has the aggregated data ready to bulk load into the index
        # or even multiple indexes
        print(len(user_docs.keys()))


@app.timer(interval=0.5)
async def example_sender(app):
    """
    Used to simulate user interactions with the system like username change, add follower, etc
    :param app:
    :return:
    """
    print("preparing msg")
    user_id_lst = ['er56kmn', 'oiuh76n', 'df47kj']

    user_id = random.choice(user_id_lst)
    extra_kwargs = {}
    if random.randint(0,10) >= 5:
        extra_kwargs['username'] = f'c{base64.urlsafe_b64encode(os.urandom(6)).decode()}'

    user_event = UserEvent(
        user_id=user_id,
        follower_id=f'c{base64.urlsafe_b64encode(os.urandom(6)).decode()}',
        following_id=f'c{base64.urlsafe_b64encode(os.urandom(6)).decode()}',
        **extra_kwargs
    )
    await topic.send(
        key=user_id,
        value=user_event,
    )
    print("sent msg")


if __name__ == '__main__':
    app.main()

 

相關文章