This page looks best with JavaScript enabled

Deploying Elasticsearch on Memory Storage - 100M+ Records, Full-Text Search in 100ms

 ·  ☕ 11 min read

1. Mount a Memory Storage Directory on the Host

  • Create a directory to mount
1
mkdir /mnt/memory_storage
  • Mount the tmpfs filesystem
1
mount -t tmpfs -o size=800G tmpfs /mnt/memory_storage

Storage is allocated on demand, so using 100G of storage only consumes 100G of memory. The host node has 2T of memory, and here 800G is allocated to store Elasticsearch data.

  • Create the directories in advance
1
2
3
mkdir /mnt/memory_storage/elasticsearch-data-es-jfs-prod-es-default-0
mkdir /mnt/memory_storage/elasticsearch-data-es-jfs-prod-es-default-1
mkdir /mnt/memory_storage/elasticsearch-data-es-jfs-prod-es-default-2

If the directories are not created in advance and given read/write permissions, the Elasticsearch component will fail to start, complaining that multiple nodes use the same data directory.

  • Configure directory permissions
1
chmod -R 777 /mnt/memory_storage
  • DD test for IO bandwidth
1
2
3
4
5
dd if=/dev/zero of=/mnt/memory_storage/dd.txt bs=4M count=2500

2500+0 records in
2500+0 records out
10485760000 bytes (10 GB, 9.8 GiB) copied, 3.53769 s, 3.0 GB/s

Clean up the file

1
rm -rf /mnt/memory_storage/dd.txt
  • FIO test for IO bandwidth
1
2
3
4
fio --name=test --filename=/mnt/memory_storage/fio_test_file --size=10G --rw=write --bs=4M --numjobs=1 --runtime=60 --time_based

Run status group 0 (all jobs):
  WRITE: bw=2942MiB/s (3085MB/s), 2942MiB/s-2942MiB/s (3085MB/s-3085MB/s), io=172GiB (185GB), run=60001-60001msec

Clean up the file

1
rm -rf /mnt/memory_storage/fio_test_file
  • Test memory IO bandwidth
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mbw 10000

Long uses 8 bytes. Allocating 2*1310720000 elements = 20971520000 bytes of memory.
Using 262144 bytes as blocks for memcpy block copy test.
Getting down to business... Doing 10 runs per test.
0	Method: MEMCPY	Elapsed: 1.62143	MiB: 10000.00000	Copy: 6167.380 MiB/s
1	Method: MEMCPY	Elapsed: 1.63542	MiB: 10000.00000	Copy: 6114.656 MiB/s
2	Method: MEMCPY	Elapsed: 1.63345	MiB: 10000.00000	Copy: 6121.997 MiB/s
3	Method: MEMCPY	Elapsed: 1.63715	MiB: 10000.00000	Copy: 6108.161 MiB/s
4	Method: MEMCPY	Elapsed: 1.64429	MiB: 10000.00000	Copy: 6081.667 MiB/s
5	Method: MEMCPY	Elapsed: 1.62772	MiB: 10000.00000	Copy: 6143.574 MiB/s
6	Method: MEMCPY	Elapsed: 1.60684	MiB: 10000.00000	Copy: 6223.379 MiB/s
7	Method: MEMCPY	Elapsed: 1.62499	MiB: 10000.00000	Copy: 6153.876 MiB/s
8	Method: MEMCPY	Elapsed: 1.63967	MiB: 10000.00000	Copy: 6098.770 MiB/s
9	Method: MEMCPY	Elapsed: 2.97213	MiB: 10000.00000	Copy: 3364.588 MiB/s
AVG	Method: MEMCPY	Elapsed: 1.76431	MiB: 10000.00000	Copy: 5667.937 MiB/s
0	Method: DUMB	Elapsed: 1.01521	MiB: 10000.00000	Copy: 9850.140 MiB/s
1	Method: DUMB	Elapsed: 0.85378	MiB: 10000.00000	Copy: 11712.605 MiB/s
2	Method: DUMB	Elapsed: 0.82487	MiB: 10000.00000	Copy: 12123.167 MiB/s
3	Method: DUMB	Elapsed: 0.84520	MiB: 10000.00000	Copy: 11831.463 MiB/s
4	Method: DUMB	Elapsed: 0.83050	MiB: 10000.00000	Copy: 12040.968 MiB/s
5	Method: DUMB	Elapsed: 0.84932	MiB: 10000.00000	Copy: 11774.194 MiB/s
6	Method: DUMB	Elapsed: 0.82491	MiB: 10000.00000	Copy: 12122.505 MiB/s
7	Method: DUMB	Elapsed: 1.44235	MiB: 10000.00000	Copy: 6933.144 MiB/s
8	Method: DUMB	Elapsed: 2.68656	MiB: 10000.00000	Copy: 3722.225 MiB/s
9	Method: DUMB	Elapsed: 8.44667	MiB: 10000.00000	Copy: 1183.898 MiB/s
AVG	Method: DUMB	Elapsed: 1.86194	MiB: 10000.00000	Copy: 5370.750 MiB/s
0	Method: MCBLOCK	Elapsed: 4.52486	MiB: 10000.00000	Copy: 2210.013 MiB/s
1	Method: MCBLOCK	Elapsed: 4.82467	MiB: 10000.00000	Copy: 2072.683 MiB/s
2	Method: MCBLOCK	Elapsed: 0.84797	MiB: 10000.00000	Copy: 11792.870 MiB/s
3	Method: MCBLOCK	Elapsed: 0.84980	MiB: 10000.00000	Copy: 11767.516 MiB/s
4	Method: MCBLOCK	Elapsed: 0.87665	MiB: 10000.00000	Copy: 11407.113 MiB/s
5	Method: MCBLOCK	Elapsed: 0.85952	MiB: 10000.00000	Copy: 11634.468 MiB/s
6	Method: MCBLOCK	Elapsed: 0.84132	MiB: 10000.00000	Copy: 11886.154 MiB/s
7	Method: MCBLOCK	Elapsed: 0.84970	MiB: 10000.00000	Copy: 11768.915 MiB/s
8	Method: MCBLOCK	Elapsed: 0.86918	MiB: 10000.00000	Copy: 11505.150 MiB/s
9	Method: MCBLOCK	Elapsed: 0.85996	MiB: 10000.00000	Copy: 11628.434 MiB/s
AVG	Method: MCBLOCK	Elapsed: 1.62036	MiB: 10000.00000	Copy: 6171.467 MiB/s

It seems the IO bandwidth of memory mounted as a filesystem only reaches about half the IO bandwidth of raw memory.

2. Create PVCs on the Kubernetes Cluster

  • Configure environment variables
1
2
export NAMESPACE=data-center
export PVC_NAME=elasticsearch-data-es-jfs-prod-es-default-0
  • Create the PV and PVC
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
kubectl create -f - <<EOF
apiVersion: v1
kind: PersistentVolume
metadata:
  name: ${PVC_NAME}
  namespace: ${NAMESPACE}
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 800Gi
  hostPath:
    path: /mnt/memory_storage/${PVC_NAME}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${PVC_NAME}
  namespace: ${NAMESPACE}
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 800Gi
EOF

By changing the PVC_NAME variable, create at least 3 PVC applications; in the end I created 10 PVCs, providing 15+ TB of storage in total.

3. Deploy the Elasticsearch Components

Some content is omitted here; for details see Storing Elasticsearch Data in JuiceFS.

  • Deploy Elasticsearch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  namespace: $NAMESPACE
  name: es-jfs-prod
spec:
  version: 8.12.0
  image: elasticsearch:8.12.0
  http:
    tls:
      selfSignedCertificate:
        disabled: true
  nodeSets:
  - name: default
    count: 3
    config:
      node.store.allow_mmap: false
      index.store.type: niofs
    podTemplate:
      spec:
        nodeSelector:
          servertype: Ascend910B-24
        initContainers:
        - name: sysctl
          securityContext:
            privileged: true
            runAsUser: 0
          command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
        - name: install-plugins
          command:
            - sh
            - -c
            - |
              bin/elasticsearch-plugin install --batch https://get.infini.cloud/elasticsearch/analysis-ik/8.12.0 &&
              bin/elasticsearch-plugin install --batch repository-s3 &&
              bin/elasticsearch-plugin install --batch analysis-icu &&
              echo xxx | bin/elasticsearch-keystore add --stdin --force s3.client.default.secret_key &&
              echo xxx | bin/elasticsearch-keystore add --stdin --force s3.client.default.access_key
          securityContext:
            runAsUser: 0
            runAsGroup: 0
        containers:
        - name: elasticsearch
          readinessProbe:
            exec:
              command:
              - bash
              - -c
              - /mnt/elastic-internal/scripts/readiness-probe-script.sh
            failureThreshold: 10
            initialDelaySeconds: 30
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 30
          env:
            - name: "ES_JAVA_OPTS"
              value: "-Xms31g -Xmx31g"
            - name: "NSS_SDB_USE_CACHE"
              value: "no"
          resources:
            requests:
              cpu: 8
              memory: 64Gi
EOF
  • View the Elasticsearch password
1
2
3
kubectl -n $NAMESPACE get secret es-jfs-prod-es-elastic-user -o go-template='{{.data.elastic | base64decode}}'

xxx

The default username is elastic

  • Deploy Metricbeat
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
kubectl apply -f - <<EOF
apiVersion: beat.k8s.elastic.co/v1beta1
kind: Beat
metadata:
  name: es-jfs-prod
  namespace: $NAMESPACE
spec:
  type: metricbeat
  version: 8.12.0
  elasticsearchRef:
    name: es-jfs-prod
  config:
    metricbeat:
      autodiscover:
        providers:
          - type: kubernetes
            scope: cluster
            hints.enabled: true
            templates:
              - config:
                  - module: kubernetes
                    metricsets:
                      - event
                    period: 10s
    processors:
    - add_cloud_metadata: {}
    logging.json: true
  deployment:
    podTemplate:
      spec:
        serviceAccountName: metricbeat
        automountServiceAccountToken: true
        securityContext:
          runAsUser: 0
        containers:
        - name: metricbeat
          resources:
            requests:
              cpu: 1
              memory: 1Gi
            limits:
              cpu: 4
              memory: 4Gi
EOF
  • Deploy Kibana
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  namespace: $NAMESPACE
  name: es-jfs-prod
spec:
  version: 8.12.0
  count: 1
  image: elastic/kibana:8.12.0
  elasticsearchRef:
    name: es-jfs-prod
  http:
    tls:
      selfSignedCertificate:
        disabled: true
EOF
  • View the Elasticsearch cluster information

  • Configure S3 storage

Mainly used to back up and restore indices

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
PUT /_snapshot/ks3
{
  "type": "s3",
  "settings": {
    "bucket": "xxx",
    "base_path": "datalake/prod/elastic-snapshot",
    "region": "BEIJING",
    "endpoint": "ks3-cn-beijing-internal.ksyuncs.com"
  }
}

4. Import Data

  • Create an index

Run this in the Dev Tools page of Elasticsearch Management:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
PUT /bayou_tt_articles
{
  "settings": {
    "index": {
      "number_of_shards": 30,
      "number_of_replicas": 1,
      "refresh_interval": "120s",
      "translog.durability": "async",
      "translog.sync_interval": "120s",
      "translog.flush_threshold_size": "2048M"
    }
  },
  "mappings": {
	 "properties": {
      "text": {
        "type": "text",
        "analyzer": "ik_smart"
      }
    }
  }
}

There are two things to watch out for:

  1. Keep the size of each shard between 10-50G. Here number_of_shards is set to 30 because there are several hundred GB of data to import in total.
  2. The replica count should be at least 1, to ensure that no data is lost while Pods are being rolled. When a Pod’s IP changes, Elasticsearch treats it as a new node and cannot reuse the previous data; at that point, if there is no replica to rebuild the shards, data will be lost.
  • Install the import tool

You can also use the elasticdump container to import, and an example of that is given below. Here npm is used for the installation.

1
apt-get install npm -y
1
npm install elasticdump -g
  • Import the data
1
2
export DATAPATH=./bayou_tt_articles_0.jsonl
nohup elasticdump --limit 20000 --input=${DATAPATH} --output=http://elastic:xxx@x.x.x.x:31391/ --output-index=bayou_tt_articles --type=data --transform="doc._source=Object.assign({},doc)" > elasticdump-${DATAPATH}.log 2>&1 &

limit is the number of records imported per batch; the default value of 100 is too small, so it is recommended to make it as large as possible on the premise that the import still succeeds.

  • Check the indexing rate

The indexing rate reached 1w+/s, but the ceiling is far beyond that. According to the community documentation’s stress test results, a single node can provide an indexing rate of at least 2W/s.

5. Testing and Verification

  • Full-text search performance improved significantly

The figure above shows a full-text search speed of 18s with JuiceFS storage, and 5s with Elasticsearch on SSD nodes. The figure below shows a full-text search speed of around 100ms with memory storage.

  • Updating Elasticsearch does not lose data

The CPU and Memory previously allocated to the Elasticsearch Pod were too generous, so they were adjusted to CPU 32C and Memory 64 GB. During the rolling update, Elasticsearch stayed available the whole time and no data was lost.

But be sure to set replicas > 1, and avoid restarting Pods on your own; even though a Pod is updated on its original node.

  • Node scale-out can be done smoothly

Since the business’s total Elasticsearch storage requirement is around 10T, I kept adding nodes up to 10, and Elasticsearch’s index shards migrated automatically and were distributed evenly across these nodes.

  • Export speed reaches 1w records per second
1
docker run --rm -ti elasticdump/elasticsearch-dump --limit 10000 --input=http://elastic:xxx@x.x.x.x:31391/bayou_tt_articles --output=/data/es-bayou_tt_articles-output.json --type=data
1
2
3
4
5
6
7
Wed, 29 May 2024 01:41:23 GMT | got 10000 objects from source elasticsearch (offset: 0)
Wed, 29 May 2024 01:41:23 GMT | sent 10000 objects to destination file, wrote 10000
Wed, 29 May 2024 01:41:24 GMT | got 10000 objects from source elasticsearch (offset: 10000)
Wed, 29 May 2024 01:41:24 GMT | sent 10000 objects to destination file, wrote 10000
Wed, 29 May 2024 01:41:25 GMT | got 10000 objects from source elasticsearch (offset: 20000)
Wed, 29 May 2024 01:41:25 GMT | sent 10000 objects to destination file, wrote 10000
Wed, 29 May 2024 01:41:25 GMT | got 10000 objects from source elasticsearch (offset: 30000)

The export speed can reach 1w records per second; 100 million records take about 3h, which basically meets the needs of index backup and migration.

  • Elasticsearch node Pods do not drift when updated

The Pod distribution across nodes before the update is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
NAME                                           READY   STATUS    RESTARTS      AGE   IP               NODE                         NOMINATED NODE   READINESS GATES
es-jfs-prod-beat-metricbeat-7fbdd657c4-djgg6   1/1     Running   6 (32m ago)   18h   10.244.54.5      ascend-01   <none>           <none>
es-jfs-prod-es-default-0                       1/1     Running   0             28m   10.244.46.82     ascend-07   <none>           <none>
es-jfs-prod-es-default-1                       1/1     Running   0             29m   10.244.23.77     ascend-53   <none>           <none>
es-jfs-prod-es-default-2                       1/1     Running   0             31m   10.244.49.65     ascend-20   <none>           <none>
es-jfs-prod-es-default-3                       1/1     Running   0             32m   10.244.54.14     ascend-01   <none>           <none>
es-jfs-prod-es-default-4                       1/1     Running   0             34m   10.244.100.239   ascend-40   <none>           <none>
es-jfs-prod-es-default-5                       1/1     Running   0             35m   10.244.97.201    ascend-39   <none>           <none>
es-jfs-prod-es-default-6                       1/1     Running   0             37m   10.244.101.156   ascend-38   <none>           <none>
es-jfs-prod-es-default-7                       1/1     Running   0             39m   10.244.19.101    ascend-49   <none>           <none>
es-jfs-prod-es-default-8                       1/1     Running   0             40m   10.244.16.109    ascend-46   <none>           <none>
es-jfs-prod-es-default-9                       1/1     Running   0             41m   10.244.39.119    ascend-15   <none>           <none>
es-jfs-prod-kb-75f7bbd96-6tcrn                 1/1     Running   0             18h   10.244.1.164     ascend-22   <none>           <none>

The Pod distribution across nodes after the update is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
NAME                                           READY   STATUS    RESTARTS      AGE     IP               NODE                         NOMINATED NODE   READINESS GATES
es-jfs-prod-beat-metricbeat-7fbdd657c4-djgg6   1/1     Running   6 (50m ago)   18h     10.244.54.5      ascend-01   <none>           <none>
es-jfs-prod-es-default-0                       1/1     Running   0             72s     10.244.46.83     ascend-07   <none>           <none>
es-jfs-prod-es-default-1                       1/1     Running   0             2m35s   10.244.23.78     ascend-53   <none>           <none>
es-jfs-prod-es-default-2                       1/1     Running   0             3m59s   10.244.49.66     ascend-20   <none>           <none>
es-jfs-prod-es-default-3                       1/1     Running   0             5m34s   10.244.54.15     ascend-01   <none>           <none>
es-jfs-prod-es-default-4                       1/1     Running   0             7m21s   10.244.100.240   ascend-40   <none>           <none>
es-jfs-prod-es-default-5                       1/1     Running   0             8m44s   10.244.97.202    ascend-39   <none>           <none>
es-jfs-prod-es-default-6                       1/1     Running   0             10m     10.244.101.157   ascend-38   <none>           <none>
es-jfs-prod-es-default-7                       1/1     Running   0             11m     10.244.19.102    ascend-49   <none>           <none>
es-jfs-prod-es-default-8                       1/1     Running   0             13m     10.244.16.110    ascend-46   <none>           <none>
es-jfs-prod-es-default-9                       1/1     Running   0             14m     10.244.39.120    ascend-15   <none>           <none>
es-jfs-prod-kb-75f7bbd96-6tcrn                 1/1     Running   0             18h     10.244.1.164     ascend-22   <none>           <none>

This laid one of my concerns to rest: when an Elasticsearch Pod restarts and drifts, will shard data remain on the node and cause memory usage to keep ballooning? The answer is no. The ECK Operator seems to be able to restart the Pod on its original node, and the mounted Hostpath data remains valid for the new Pod; data is only lost when the host node itself restarts.

6. Summary

AI compute nodes have a lot of idle CPU and Memory resources. Using these high-memory host nodes to deploy some short-lived, high-performance applications backed by memory storage helps improve resource utilization efficiency.

This article mainly introduced a solution for deploying Elasticsearch with memory storage via Hostpath to provide high-performance query capability. The details are as follows:

  1. mount a memory directory onto the host
  2. Create a Hostpath-based PVC and mount the data into the above directory
  3. Deploy Elasticsearch using the ECK Operator
  4. When Elasticsearch is updated, data is not lost, but multiple host nodes must not be restarted at the same time
  5. For 300+GB and 100M+ records, in a full-text search response scenario, the speed with JuiceFS storage is 18s, with SSD nodes it is 5s, and with memory nodes it is 100ms

微信公众号
WRITTEN BY
微信公众号