Cùi Bắp
bởi Cùi Bắp
13 phút để đọc

Chuyên mục

Tags

Cấp cứu, code lỗi đi cmn hết dữ liệu Elasticsearch trên production rồi anh :rofl:

Không sớm thì muộn ai trong chúng ta cũng sẽ được nghe câu nói trên :joy:. Phốt everywhere :fire:

Cách đây không lâu mình và khách hàng phải xây dựng một hệ thống nội bộ cho họ dùng để tìm kiếm, thống kê dữ liệu trong Elasticsearch (đại loại như Kibana), tuy không nhiều chức năng như Kibana nhưng công việc này đã cho mình rất nhiều kiến thức về Elasticsearch.

Hey, what’s up man?

Với vai trò là người quản lý server của khách, thì việc biết cách sao lưu (backup or snapshot), phục hồi (restore), đánh lại chỉ mục (re-index), index aliases & zero downtime (éo biết dịch thế nào, cụ thể là bạn tạo 1 cái mặt nạ, người dùng nhìn vào cái mặt nạ đó nhưng méo biết đằng sau nó là index nào, mặc cho admin switch mệt nghỉ :clown_face:)

Trong bài viết này tôi sẽ mô tả cách mà tôi thực hiện việc sao lưu và đánh lại index trên Elasticsearch.

Nếu máy tính bạn không có Elasticsearch, Kibana để thực hành thì đừng lo, hãy cài docker/docker-compose và dùng sẵn stack này nhé.

Sau khi chạy lệnh $ docker-compose up , đợi các service ready hết các bạn vào Kibana Dev Console là có thể bắt tay thử liền.

Prepare

Trước hết hãy chuẩn bị sẵn dữ liệu mẫu để thao tác. Ở đây mình sẽ tạo sẵn một index tên là messages_v1, chứa toàn bộ dữ liệu về tin nhắn chat của mình chẳng hạn :slightly_frowning_face:

PUT messages_v1
{
  "settings" : {
    "analysis" : {
      "analyzer":{
        "custom_analyzer":{
           "type": "custom",
           "filter": [
              "lowercase"
           ],
           "tokenizer": "whitespace"
        }
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "content": {
          "type": "text",
          "analyzer": "custom_analyzer"
        },
        "created_at": {
          "type":   "date",
          "format": "yyyy-MM-dd HH:mm:ss"
        }
      }
    }
  }
}

Sau đó tạo thêm đôi ba tin nhắn vào index này:

POST /messages_v1/doc/_bulk?pretty
{ "create": { "_id" : "1" } }
{ "content": "Thank you for being a friend.", "created_at": "2018-06-11 23:43:00" }
{ "create": { "_id" : "2" } }
{ "content": "Cảm ơn vì Cảm ơn vì đã làm bạn với tôi.", "created_at": "2018-06-12 15:12:23" }
{ "create": { "_id" : "3" } }
{ "content": "友達になってくれてありがとうございます。", "created_at": "2018-06-16 09:02:13" }
{ "create": { "_id" : "4" } }
{ "content": "I am looking forward to seeing my friends tomorrow.", "created_at": "2018-06-14 23:43:00" }
{ "create": { "_id" : "5" } }
{ "content": "Tôi rất mong được gặp bạn bè vào ngày mai.", "created_at": "2018-06-15 16:42:55" }
{ "create": { "_id" : "6" } }
{ "content": "私は明日友達と会うことを楽しみにしている。", "created_at": "2018-06-15 15:12:23" }

Index aliases & Zero downtime

Tại sao mình lại đưa phần này lên đầu tiên, đơn giản vì nó khá quan trọng :point_down: Vấn đề ở chỗ khi có 1 cục dữ liệu đã index mà bạn muốn index lại hay chỉ đơn giản là update lại tên của cục dữ liệu đó, thì cũng sẽ mất một khoảng thời gian nhất định. Tất nhiên với tư cách là một người sử dụng, ko ai muốn nhìn thấy dòng chữ "The system is in maintenance mode. Please contact your administrator or try again later." :))

Index alias giống như shortcut hay symbolic link, trỏ tới một hay nhiều indices, có thể được sử dụng như index name trong mọi API. Aliases cho chúng ta sự linh hoạt khi sử dụng Elasticsearch như:

  • Chuyển đổi từ một index này sang một index khác trên cluster
  • Nhóm nhiều indices lại (ví dụ, last_three_months)
  • Tạo “views” trên một tập con documents trong index
PUT /messages_v1/_alias/messages

Ở đây mình tạo aliasmessages cho index messages_v1. Thử search với alias phát :mag:

GET /messages/_search?pretty=true

Ngược lại ta có thể kiểm tra xem alias: messages point tới index nào :point_right:

GET /*/_alias/messages

Vậy khi muốn cập nhật lại alias, trỏ sang 1 index khác thì sao ? Vui lòng đọc phần này nhé :joy:

Snapshot/Backup

Khi sao lưu dữ liệu bạn cần nhớ một điều:

  • Một snapshot của index được tạo trong bản 5.x thì có thể phục hồi trên bản 6.x.
  • Một snapshot của index được tạo trong bản 2.x thì có thể phục hồi trên bản 5.x.
  • Một snapshot của index được tạo trong bản 1.x thì có thể phục hồi trên bản 2.x.

Repository

Bạn cần tạo repository trước khi sao lưu hay phục hổi dữ liệu. Elastic khuyến khích người dùng nên tạo mới snapshot repository cho mỗi phiên bản elasticsearch quan trọng. Repository hợp lệ phụ thuộc vào kiểu của repository.

Nếu bạn đăng ký một snapshot cho nhiều cluster thì chỉ duy nhất 1 cluster là có quyền write vào repository. Tất các các clusters khác kết nối vào repository để chỉ có quyền readonly.

  • Tạo folder backup. Nếu bạn không dùng stack này hãy dùng lệnh sau:
    mkdir -p /usr/share/elasticsearch/backups
    
  • Đăng ký backup location trên master và các nodes của Elasticsearch bằng cách thêm dòng sau vào file elasticsearch.yml
    path.repo: ["/usr/share/elasticsearch/backups"]
    
  • Tạo repository
    PUT /_snapshot/messages_backup
    {
    "type": "fs",
    "settings": {
      "location": "/usr/share/elasticsearch/backups"
    }
    }
    

  • Tiến hành sao lưu dữ liệu trong index messages_v1
    PUT /_snapshot/messages_backup/snapshot_1?wait_for_completion=true
    {
    "indices": "messages_v1",
    "ignore_unavailable": true,
    "include_global_state": false
    }
    

    Tham số wait_for_completion được thêm vào đồng nghĩa với việc khi nào request trả về response thì việc backup mới hoàn tất.

Restore

Một snapshot có thể được restore (phục hồi) bởi câu lệnh sau:

POST /_snapshot/messages_backup/snapshot_1/_restore

Câu lệnh này sẽ khôi phục lại dữ liệu của index đã backup với nguyên trạng cả tên index, … Nên nếu trong hệ thống có index trùng tên với index trong snapshot thì chúng ta sẽ gặp lỗi:

{
  "error": {
    "root_cause": [
      {
        "type": "snapshot_restore_exception",
        "reason": "[messages_backup:snapshot_1/T2pJRLWfTVeclg3_EdcLSQ] cannot restore index [messages_v1] because an open index with same name already exists in the cluster. Either close or delete the existing index or restore the index under a different name by providing a rename pattern and replacement name"
      }
    ],
    "type": "snapshot_restore_exception",
    "reason": "[messages_backup:snapshot_1/T2pJRLWfTVeclg3_EdcLSQ] cannot restore index [messages_v1] because an open index with same name already exists in the cluster. Either close or delete the existing index or restore the index under a different name by providing a rename pattern and replacement name"
  },
  "status": 500
}

Do đó nếu muốn restore snapshot, nhưng đổi tên index ta có thể dùng lệnh chi tiết hơn

POST /_snapshot/messages_backup/snapshot_1/_restore
{
  "indices": "messages_v1",
  "ignore_unavailable": true,
  "include_global_state": false,
  "rename_pattern": "messages_(.+)",
  "rename_replacement": "restored_messages_$1"
}
  • Elasticsearch sẽ dựa vào pattern khai báo trong rename_pattern để tìm index có tên tương ứng và thay thế theo quy tắc khai báo trong rename_replacement.

Sau khi chạy câu lệnh trên ta có kết quả sau: List all index ta thấy :stuck_out_tongue_closed_eyes: ほら restored_messages_v1

Re-index

Cho dù bạn có thể thêm các type (kiểu) mới vào chỉ mục hoặc thêm các field (trường) mới vào type (kiểu), thì bạn cũng không thể làm điều đó với analyzers cũng như áp dụng các thay đổi cho các trường hiện có. Nếu bạn làm như vậy, dữ liệu đã được lập chỉ mục sẽ không chính xác và các tìm kiếm của bạn sẽ không còn hoạt động như mong đợi. :smile:

Vâng, đó chính là lý do chúng ta cần tới chức năng Reindex. Trên documentation của Elasticsearch: The Definitive Guide có mô tả

To reindex all of the documents from the old index efficiently, use scroll to retrieve batches of documents from the old index, and the bulk API to push them into the new index.

Đại loại là họ khuyên chúng ta nên sử dụng scroll để lấy từng mảng dữ liệu document nhỏ, sau đó mới push vào new_index. Mình thấy cách này hay đấy nhưng tốn công quá =))) Tuy nhiên với các hệ thống có lượng dữ liệu lớn thì bạn nên làm theo cách mà tài liệu khuyến khích nhé :grinning:

Ta làm đơn giản hơn:

  • Tạo một index mới. Ở đây tôi tạo index có tên là messages_v2 với việc sử dụng bộ analyzer có tên là kuromoji, giúp phân tách tiếng Nhật tốt hơn.
      PUT messages_v2
      {
        "settings" : {
          "analysis" : {
            "analyzer":{
              "custom_analyzer":{
                 "type": "custom",
                 "tokenizer": "kuromoji_tokenizer"
              }
            }
          }
        },
        "mappings": {
          "doc": {
            "properties": {
              "content": {
                "type": "text",
                "analyzer": "custom_analyzer"
              },
              "created_at": {
                "type":   "date",
                "format": "yyyy-MM-dd HH:mm:ss"
              }
            }
          }
        }
      }
    
  • Copy dữ liệu từ index cũ sang index mới và đánh chỉ mục lại
      POST _reindex
      {
        "source": {
          "index": "messages_v1"
        },
        "dest": {
          "index": "messages_v2"
        }
      }
    

  • Một bước quan trọng để hệ thống không bị down time (gián đoạn) đó là cập nhật lại alias sang index mới
      POST /_aliases
      {
        "actions": [
          { "remove": { "index": "messages_v1", "alias": "messages" }},
          { "add":    { "index": "messages_v2", "alias": "messages" }}
        ]
      }
    

  • Xác nhận lại alias messages phát :sunglasses:
      GET /*/_alias/messages
    

    WTF ! Hoá ra là lúc restore snapshot nó phục hồi luôn cả trạng thái alias trước đó. Cụ thể là messages trước point tới messages_v1 thì giờ có cả restored_messages_v1.

    Chuẩn cmnr, mình thử search all phát ra gấp đôi số lượng bản ghi:

      GET /messages/_search?pretty=true
    

    Ta remove luôn, tránh gây hậu hoạ về sau :triumph:

      POST /_aliases
      {
        "actions": [
          { "remove": { "index": "restored_messages_v1", "alias": "messages" }}
        ]
      }
    

Sau khi thực hiện reindex và switch lại alias index sang messages_v2. Hãy thử search với 1 keyword tiếng Nhật. Ví dụ: 友達 (Bạn bè)

Với bản messages_v1

GET /messages_v1/_search?pretty=true
{
  "query": {
    "match" : {
      "content" : "友達"
    }
  }
}

Méo có kết quả nào do dùng bộ analyzer với tokenizer là dấu cách.

Với bản messages (hay thực chất là messages_v2)

GET /messages/_search?pretty=true
{
  "query": {
    "match" : {
      "content" : "友達"
    }
  }
}

Các bạn biết tại sao lại thế hok, đơn giản thôi, tôi dùng kuromoji và nó tách các term tiếng Nhật ra chuẩn vãi lềnh :joy:

Để test các term được sinh ra như thế nào, bạn có thể dùng lệnh sau để xem chi tiết term trong 1 document:

GET /messages_v2/doc/3/_termvectors?fields=content

hoặc với mục đích testing analyzer thì 1/2 câu lệnh sau ngắn gọn hơn:

POST _analyze
{
  "tokenizer": "kuromoji_tokenizer",
  "text": "友達になってくれてありがとうございます"
}

POST messages_v2/_analyze
{
  "analyzer": "custom_analyzer",
  "text":     "友達になってくれてありがとうございます"
}

Bài viết trên được áp dụng trong trường hợp 1 node Elasticsearch nên mọi thứ chạy = cơm vẫn rất ổn thoả, nếu bạn sắp phải thao tác trên các hệ thống replica shards cần lưu ý thêm như:

  • Tắt shard allocation trên cluster
  • Thực hiện index flush sync …
  • Graceful shutdown & restart & rejoin cluster
  • Mở lại shard allocation trên cluster
  • Cluster heathcheck → :green_apple: green
  • Kế hoạch rollback nếu việc update bị fail :sweat_smile:

Tất nhiên để thực hiện chính xác và không bị mắc sai sót, ta nên tự động hoá các công việc đó với một vài công cụ như shell-script, ansible, puppet, … Chúc bạn thực hiện thành công =))

References