4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズパイ2台で無停止サーバ (slee-Pi 3 で物理的にフェンシングして HA クラスタ化)

Last updated at Posted at 2024-06-28

はじめに

メカトラックス株式会社maeda01です。

弊社製品 slee-Pi 3 は電源管理・死活監視モジュールとして広く用いられています。

slee-Pi 3 は基板上に外部入力端子 (CN4) と外部出力端子 (CN5) を備えているため外部の機器を接続することが可能です。
また、外部入力端子は設定を行うことで外部信号による強制的な電源断が可能です。
そのため、CN4 と CN5 を互いに接続して STONITH1 する、物理的にフェンシング可能な HA (High Availability) クラスタをラズベリーパイで構築できます。
本記事は slee-Pi 3 の外部入力機能と外部出力機能を使用して STONITH によるフェンシングを行う方法を紹介します。

やりたいこと

  • 1日1回程度 Raspberry Pi を再起動させたいが、その間サービス (Docker コンテナ) を停止させないようにする (具体的には以下のようなシナリオになります)

    • (Raspberry Pi A 側) 再起動のためにコンテナを停止
    • (Raspberry Pi B 側) Raspberry Pi B のコンテナを起動してフェイルオーバー
    • (Raspberry Pi A 側) 再起動が完了してコンテナを起動
    • (Raspberry Pi B 側) Raspberry Pi B のコンテナを停止してフェイルバック
  • Raspberry Pi A または B がハードウェア障害によって生存確認ができなくなった場合に再起動させる

  • 上記のシナリオで Raspberry Pi B がフェイルバックに失敗してスプリットブレインになってしまった場合に Raspberry Pi B を再起動させる (具体的には以下のようなシナリオになります)

    • クラスタのノードに問題が発生したことを検知して、STONITH によるフェンシングを行う

使用するもの

  • Raspberry Pi 4 model B + slee-Pi 3: 2台
  • micro SD カード: 2枚
  • AC アダプタ (12V/2A): 2台
  • 外部入力/出力接続用ハーネス: 2本
    • slee-Pi 3 の外部入力/出力用コネクタには EHR-2 が嵌合します
  • LAN ケーブル: 2本

OS は Raspberry Pi OS Debian GNU/Linux 12 (bookworm) 64bit 版を使用しています。

機器構成

2台の slee-Pi 3 の CN4 と CN5 をハーネスで接続します。
Raspberry Pi 2台ともに同一セグメントの IP アドレスが割り当てられるように有線 LAN を接続します。
機器構成の図としては以下のようになります。

image.png

slee-Pi 3 のセットアップ

以下のリンクに従って slee-Pi 3 のセットアップを行います。

クラスタ作成

Raspberry Pi 2台を区別するため、ホスト名 (ノード名) をそれぞれ raspberrypi-1, raspberrypi-2 としています。

パッケージのインストール

raspberrypi-1, raspberrypi-2 の両方で行います。

sudo apt install -y pacemaker corosync pcs

pacemaker をインストールするとユーザー hacluster が追加されるのでパスワードを設定します。

mtx@raspberrypi-1:~ $ sudo passwd hacluster
New password: 
Retype new password: 

pcs のサービスを有効化します。

mtx@raspberrypi-1:~ $ sudo systemctl enable pcsd

Pacemaker, Corosync の詳細については以下をご参照ください。

pcs コマンドの詳細は以下のドキュメントをご参照ください。

hosts の設定

raspberrypi-1, raspberrypi-2 の両方で行います。
クラスタに登録するノード (Raspberry Pi のホスト名) を /etc/hosts に記述します。
自ホスト名 (raspberrypi-1, raspberrypi-2) が127.0.0.1に存在しないように注意してください。

mtx@raspberrypi-1:~ $ cat /etc/hosts
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

192.168.11.211  raspberrypi-1
192.168.11.179  raspberrypi-2

クラスタの設定

デフォルトのクラスタなどがすでに存在する場合はクラスタを削除してください。

mtx@raspberrypi-1:~ $ sudo pcs cluster destroy

ユーザー hacluster がノード raspberrypi-1, raspberrypi-2 に対してログインできるように認証をします。

mtx@raspberrypi-1:~ $ sudo pcs host auth raspberrypi-1 raspberrypi-2 -u hacluster
Password:
raspberrypi-1: Authorized
raspberrypi-2: Authorized

新しいクラスタを作成していきます。
クラスタ名は apachecluster とし、ノード raspberrypi-1, raspberrypi-2 を追加します。

mtx@raspberrypi-1:~ $ sudo pcs cluster setup apachecluster raspberrypi-1 raspberrypi-2
No addresses specified for host 'raspberrypi-1', using 'raspberrypi-1'
No addresses specified for host 'raspberrypi-2', using 'raspberrypi-2'
Destroying cluster on hosts: 'raspberrypi-1', 'raspberrypi-2'...
raspberrypi-2: Successfully destroyed cluster
raspberrypi-1: Successfully destroyed cluster
Requesting remove 'pcsd settings' from 'raspberrypi-1', 'raspberrypi-2'
raspberrypi-1: successful removal of the file 'pcsd settings'
raspberrypi-2: successful removal of the file 'pcsd settings'
Sending 'corosync authkey', 'pacemaker authkey' to 'raspberrypi-1', 'raspberrypi-2'
raspberrypi-1: successful distribution of the file 'corosync authkey'
raspberrypi-1: successful distribution of the file 'pacemaker authkey'
raspberrypi-2: successful distribution of the file 'corosync authkey'
raspberrypi-2: successful distribution of the file 'pacemaker authkey'
Sending 'corosync.conf' to 'raspberrypi-1', 'raspberrypi-2'
raspberrypi-1: successful distribution of the file 'corosync.conf'
raspberrypi-2: successful distribution of the file 'corosync.conf'
Cluster has been successfully set up.

すべてのノードでクラスタを起動します。

mtx@raspberrypi-1:~ $ sudo pcs cluster start --all
raspberrypi-1: Starting Cluster...
raspberrypi-2: Starting Cluster...

クラスタの設定をしていきます。
stonith-enabled=ture として、STONITH の有効化を行います。
3ノード以上のクラスタの場合はクォーラムポリシーの設定も行います。今回は no-quorum-policy=ignore としてノード数がクォーラム数以下になった場合もリソースを起動状態にします。

mtx@raspberrypi-1:~ $ sudo pcs property set stonith-enabled=ture
mtx@raspberrypi-1:~ $ sudo pcs property set no-quorum-policy=ignore

クラスタのステータスは以下のコマンドで確認できます。

mtx@raspberrypi-1:~ $ sudo pcs status
Cluster name: apachecluster

WARNINGS:
No stonith devices and stonith-enabled is not false

Status of pacemakerd: 'Pacemaker is running' (last updated 2024-06-21 16:43:27 +09:00)
Cluster Summary:
  * Stack: corosync
  * Current DC: raspberrypi-1 (version 2.1.5-a3f44794f94) - partition with quorum
  * Last updated: Fri Jun 21 16:43:28 2024
  * Last change:  Fri Jun 21 16:42:59 2024 by hacluster via crmd on raspberrypi-1
  * 2 nodes configured
  * 0 resource instances configured

Node List:
  * Online: [ raspberrypi-1 raspberrypi-2 ]

Full List of Resources:
  * No resources

Daemon Status:
  corosync: active/disabled
  pacemaker: active/disabled
  pcsd: active/enabled

これでクラスタの作成は完了です。

リソースの作成

ここではリソース (コンテナと IP アドレス) を作成してクラスタに割り当てます。

Docker コンテナ

以下の URL を参考に Docker をインストールします。

コンテナは、今回は Apache のコンテナを使用します。
以下のコマンドで Apache のコンテナをダウンロードします。

docker pull httpd

ダウンロードしたコンテナに名前を付けます。
今回は「httpd-test」という名前とします。

sudo docker run -d -p 80:80 --name httpd-test httpd

このコンテナ httpd-test をクラスタのリソースとして割り当てます。
リソース名は Apache_container としました。

sudo pcs resource create Apache_container ocf:heartbeat:docker image="httpd" name="httpd-test" run_opts="--name httpd-test -p 80:80"

割り当てが完了するとコンテナが起動します。
ブラウザから Raspberry Pi が持っているローカルの IP アドレスにアクセスし、Apache のデフォルトページが表示されれば OK です。

apacheデフォルトページ.jpg

仮想 IP アドレス

ここではクラスタのノード間で共有して使用する仮想的な IP アドレスを作成します。
raspberrypi-1, raspberrypi-2 のどちらのノードでコンテナが稼働していてもこの仮想 IP アドレスから Web ページにアクセスが可能になります。

以下のコマンドでクラスタに仮想 IP を作成して割り当てます。
割り当てる IP アドレスは 192.168.11.215/24 です。

mtx@raspberrypi-1:~ $ sudo pcs resource create VIP ocf:heartbeat:IPaddr2 ip=192.168.11.215 cidr_netmask=24

割り当てが完了したことを確認します。
リソースに仮想 IP (VIP という名前) があれば OK です。

mtx@raspberrypi-1:~ $ sudo pcs status
Cluster name: apachecluster
Status of pacemakerd: 'Pacemaker is running' (last updated 2024-06-25 10:59:04 +09:00)
Cluster Summary:
  * Stack: corosync
  * Current DC: raspberrypi-1 (version 2.1.5-a3f44794f94) - partition WITHOUT quorum
  * Last updated: Tue Jun 25 10:59:05 2024
  * Last change:  Tue Jun 25 10:58:56 2024 by root via cibadmin on raspberrypi-1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ raspberrypi-1 ]
  * OFFLINE: [ raspberrypi-2 ]

Full List of Resources:
  * Apache_container    (ocf:heartbeat:docker):  Started raspberrypi-1
  * VIP (ocf:heartbeat:IPaddr2):         Started raspberrypi-1

Daemon Status:
  corosync: active/disabled
  pacemaker: active/enabled
  pcsd: active/enabled

このとき、ノード raspberrypi-1 ではルーターから割り当てられた IP アドレスと作成した仮想 IP アドレスが存在します。

また、作成した仮想 IP にブラウザからアクセスすると Apache のデフォルトページが表示されます。

mtx@raspberrypi-1:~ $ ip a show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 192.168.11.211/24 brd 192.168.11.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet 192.168.11.215/24 brd 192.168.11.255 scope global secondary eth0
       valid_lft forever preferred_lft forever
    inet6 2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/64 scope global dynamic noprefixroute
       valid_lft 14335sec preferred_lft 12535sec
    inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

Apache コンテナと仮想 IP を必ず同じノードで稼働させるように設定します。

mtx@raspberrypi-1:~ $ sudo pcs constraint colocation add VIP with Apache_container INFINITY

クラスタの動作検証

ノードをオンラインからスタンバイ状態に設定

現在コンテナが稼働しているノード raspberrypi-1 をスタンバイ状態にします。

mtx@raspberrypi-1:~ $ sudo pcs node standby raspberrypi-1

コマンド実行後、ノード raspberrypi-1 がスタンバイ状態になり、ノード raspberrypi-2 でコンテナが稼働します。

mtx@raspberrypi-1:~ $ sudo pcs status
Cluster name: apachecluster

WARNINGS:
No stonith devices and stonith-enabled is not false

Status of pacemakerd: 'Pacemaker is running' (last updated 2024-06-26 09:50:37 +09:00)
Cluster Summary:
  * Stack: corosync
  * Current DC: raspberrypi-1 (version 2.1.5-a3f44794f94) - partition with quorum
  * Last updated: Wed Jun 26 09:50:38 2024
  * Last change:  Wed Jun 26 09:50:35 2024 by root via cibadmin on raspberrypi-1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Node raspberrypi-1: standby
  * Online: [ raspberrypi-2 ]

Full List of Resources:
  * Apache_container    (ocf:heartbeat:docker):  Started raspberrypi-2
  * VIP (ocf:heartbeat:IPaddr2):         Started raspberrypi-2

Daemon Status:
  corosync: active/disabled
  pacemaker: active/enabled
  pcsd: active/enabled

スタンバイ状態のノードをオンライン状態にするには以下のコマンドを実行します。

mtx@raspberrypi-1:~ $ sudo pcs node unstandby raspberrypi-1

仮想 IP とコンテナリソースの付け替え (手動)

オンライン状態のノード間でリソースを付け替えるには以下のコマンドを実行します。
ノード raspberrypi-1 からノード raspberrypi-2 にコンテナと仮想 IP を付け替えます。

mtx@raspberrypi-1:~ $ sudo pcs resource move VIP raspberrypi-2

付け替えが完了したことを確認します。

mtx@raspberrypi-1:~ $ sudo pcs status
Cluster name: apachecluster

WARNINGS:
No stonith devices and stonith-enabled is not false

Status of pacemakerd: 'Pacemaker is running' (last updated 2024-06-26 09:51:09 +09:00)
Cluster Summary:
  * Stack: corosync
  * Current DC: raspberrypi-1 (version 2.1.5-a3f44794f94) - partition with quorum
  * Last updated: Wed Jun 26 09:51:10 2024
  * Last change:  Wed Jun 26 09:51:06 2024 by root via cibadmin on raspberrypi-1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ raspberrypi-1 raspberrypi-2 ]

Full List of Resources:
  * Apache_container    (ocf:heartbeat:docker):  Started raspberrypi-2
  * VIP (ocf:heartbeat:IPaddr2):         Started raspberrypi-2

Daemon Status:
  corosync: active/disabled
  pacemaker: active/enabled
  pcsd: active/enabled

仮想 IP とコンテナリソースの付け替え (自動)

やりたいことの1つ目を行う手順になります。

  • 1日1回程度 Raspberry Pi を再起動させたいが、その間サービス (Docker コンテナ) を停止させないようにする (具体的には以下のようなシナリオになります)
    • (Raspberry Pi A 側) 再起動のためにコンテナを停止
    • (Raspberry Pi B 側) Raspberry Pi B のコンテナを起動してフェイルオーバー
    • (Raspberry Pi A 側) 再起動が完了してコンテナを起動
    • (Raspberry Pi B 側) Raspberry Pi B のコンテナを停止してフェイルバック

Raspberry Pi A はノード raspberrypi-1、Raspberry Pi B はノード raspberrypi-2とします。

raspberrypi-1 の定期的な再起動にはsystemd.timer などで実装します。
再起動前に以下の一連の流れを raspberrypi-1 で実行させます。

リソースを raspberrypi-2 に割り当てて raspberrypi-1 を再起動

mtx@raspberrypi-1:~ $ sudo pcs resource move VIP raspberrypi-2
mtx@raspberrypi-1:~ $ sudo shutdown -r now

raspberrypi-1 の再起動が完了したらリソースを raspberrypi-1 に戻す

mtx@raspberrypi-1:~ sudo pcs resource move VIP raspberrypi-1

これを raspberrypi-1 再起動完了時に実行できるようにサービス化

move-resource.service
[Unit]
Description=Move resource to raspberrypi-1
After=multi-user.target

[Service]
Type=oneshot
User=root
ExecStart=pcs resource move VIP raspberrypi-1

[Install]
WantedBy=multi-user.target

サービスの有効化

mtx@raspberrypi-1:~ $ sudo systemctl enable move-resource

これでコンテナを停止させずに raspberrypi-1 を再起動することが可能になります。

STONITH 設定

やりたいことの2つ目、3つ目を行う手順になります。

  • Raspberry Pi A または B がハードウェア障害によって生存確認ができなくなった場合に再起動させる
  • 上記のシナリオで Raspberry Pi B がフェイルバックに失敗してスプリットブレインになってしまった場合に Raspberry Pi B を再起動させる (具体的には以下のようなシナリオになります)
    • クラスタのノードに問題が発生したことを検知して、STONITH によるフェンシングを行う

外部入力による強制電源断の有効化

slee-Pi 3 はデフォルトでは外部入力による強制電源断は無効化されています。

有効化するためには /etc/default/sleepi3 を編集して EXTIN_FORCED_SHUTDOWN=1 とします。

/etc/default/sleepi3
略)
#
# Forced shutdown by external input 
#
# EXTIN_FORCED_SHUTDOWN=0 : disabled
# EXTIN_FORCED_SHUTDOWN=1 : external input has been detected 10s and shutdown
#
EXTIN_FORCED_SHUTDOWN=1

編集したら OS を再起動して /etc/default/sleepi3 の変更を反映させます。

外部入力による強制電源断が有効化されていると、外部入力が 10秒以上検出された場合に強制電源断が発生します。

fence agent の作成

slee-Pi 3 の fence agent を作成します。
ClusterLabs が提供してる fence agent の中の fence_ipmilan をベースに作成します。

以下が slee-Pi 3 の外部入力と外部出力を使用して fencing を行う agent のサンプルです。
/usr/sbin/fence_sleepi3 というファイルを作成します。

usr/sbin/fence_sleepi3
#!/bin/python3

import sys
import atexit
import time
from pathlib import Path
from importlib import machinery, util

sys.path.append("/usr/share/fence")

from fencing import *
from fencing import (fail, run_delay, EC_LOGIN_DENIED, EC_STATUS)

import sleepi

state = {"POWERED_ON": "on", 'POWERED_OFF': "off", 'SUSPENDED': "off"}

path = Path('/usr/share/sleepi3-utils/sleepi3_util.py')
path_sleepi_status = '/run/fence_sleepi3.status'

loader = machinery.SourceFileLoader(str(path), str(path))
spec = util.spec_from_file_location(str(path), path, loader=loader)
sleepi3_util = util.module_from_spec(spec)
spec.loader.exec_module(sleepi3_util)

ENVIRONMENT_FILE = '/etc/default/sleepi3'

def get_power_status(_, options):
    status = "on"
    try:
        with open(path_sleepi_status, 'r') as file:
            status = file.read()
    except:
        pass
    return status

def set_power_status(_, options):
    action = options["--action"]
    with open(path_sleepi_status, 'w') as file:
        file.write(action)
    if action == "on":
        cli.set(['extout', 1])
        time.sleep(1)
        cli.set(['extout', 0])
    elif action == "off":
        cli.set(['extout', 1])
        time.sleep(12)
        cli.set(['extout', 0])
    else:
        cli.set(['extout', 0])
    return

def reboot_cycle(_, options):
    return True

def main():
    atexit.register(atexit_handler)

    all_opt["power_wait"]["default"] = "10"

    device_opt = ["no_login", "no_password", "no_port"]
    options = check_input(device_opt, process_input(device_opt))

    docs = {}
    docs["shortdesc"] = "Fence agent for slee-Pi 3"
    docs["longdesc"] = """fence_sleepi3 is an I/O Fencing agent which can be \
used with slee-Pi 3"""
    docs["vendorurl"] = "https://mechatrax.com"
    show_docs(options, docs)

    run_delay(options)

    result = fence_action(None, options, set_power_status, get_power_status, None, reboot_cycle)

    sys.exit(result)

if __name__ == "__main__":
    env = sleepi3_util.parse_environment(ENVIRONMENT_FILE)
    bus = int(env['I2C_BUS'])
    addr = int(env['I2C_ADDRESS'], 16)
    cli = sleepi3_util.Sleepi3Cli(sleepi.Sleepi3(bus, addr))

    main()

弊社提供の slee-Pi 3 のユーティリティの中の sleepi3_util.py を使用して slee-Pi 3 の外部出力の制御をします。
cli.set(['extout', 1]) とすると外部出力がオンになり、cli.set(['extout', 0]) とすると外部出力がオフになります。

fence agent を作成したら実行権限を付与します。

mtx@raspberrypi-1:~ $ sudo chmod 755 /usr/sbin/fence_sleepi3

sleepi3_util.py では I2C を使用するため、pacemaker を実行するユーザー hacluster を i2c グループに追加します。

mtx@raspberrypi-1:~ $ sudo usermod hacluster -aG i2c
mtx@raspberrypi-1:~ $ groups hacluster
hacluster : haclient i2c

リソースの割り当て

作成した fence agent をクラスタに割り当てます。
リソース名は fence_raspi としました。

mtx@raspberrypi-1:~ $ sudo pcs stonith create fence_raspi fence_sleepi3 pcmk_host_list="raspberrypi-1 raspberrypi-2" pcmk_host_check=static-list

クラスタのステータスを確認して、割り当てが完了したことを確認します。

mtx@raspberrypi-1:~ $ sudo pcs status
Cluster name: apachecluster
Status of pacemakerd: 'Pacemaker is running' (last updated 2024-06-26 09:52:07 +09:00)
Cluster Summary:
  * Stack: corosync
  * Current DC: raspberrypi-1 (version 2.1.5-a3f44794f94) - partition with quorum
  * Last updated: Wed Jun 26 09:52:08 2024
  * Last change:  Wed Jun 26 09:52:01 2024 by root via cibadmin on raspberrypi-1
  * 2 nodes configured
  * 3 resource instances configured

Node List:
  * Online: [ raspberrypi-1 raspberrypi-2 ]

Full List of Resources:
  * Apache_container    (ocf:heartbeat:docker):  Started raspberrypi-1
  * VIP (ocf:heartbeat:IPaddr2):         Started raspberrypi-1
  * fence_raspi (stonith:fence_sleepi3):         Started raspberrypi-2

Daemon Status:
  corosync: active/disabled
  pacemaker: active/enabled
  pcsd: active/enabled

フェンシングのテスト

pcs コマンドでのテスト

登録した fence agent が機能するかテストを行います。
ノード raspberrypi-1 をフェンシングします。

mtx@raspberrypi-2:~ $ sudo pcs stonith fence raspberrypi-1

このコマンドを実行すると、raspberrypi-1 が再起動します。

slee-Pi 3 では自身の起動要因を確認できます。
今回は raspberrypi-1 の外部入力検出によって起動したので wakeup-flagextin となるはずです。

mtx@raspberrypi-1:~ $ sudo sleepi3ctl get wakeup-flag
extin

OOM からの復帰テスト

以下の記事を参考に、コンテナが稼働しているノードで OOM (Out Of Memory) を発生させます。
今回はノード raspberrypi-1 で OOM を発生させます。

※ コード中に (&amp;t) のような HTML エンティティが含まれています。コードを使用する際は該当部分を取り除いてご利用ください。

もしくは意図的にカーネルパニックを発生させます。
以下のコマンドを実行するとカーネルパニックが発生します。

mtx@raspberrypi-1:~ $ sudo su
mtx@raspberrypi-1:~ $ echo c > /proc/sysrq-trigger

正しくフェンシングの設定ができているとリソースがノード raspberrypi-2 に渡され、ノード raspberrypi-1 が再起動します。

この場合も raspberrypi-1 の外部入力検出によって起動したので wakeup-flagextin となるはずです。

mtx@raspberrypi-1:~ $ sudo sleepi3ctl get wakeup-flag
extin

※ 通常、OOM 発生時はウォッチドッグタイマが動作して再起動が発生します。
フェンシング単体のテストを行う際はこのウォッチドッグタイマを無効化してください。

ウォッチドッグタイマを無効化するには /etc/default/sleepi3 を編集して HEARTBEAT_TIMEOUT=0 とします。

/etc/default/sleepi3
略)
#
# Reboot if heartbeat signal is not received more than HEARTBEAT_TIMEOUT seconds
#
# HEARTBEAT_TIMEOUT=0 : disabled
# HEARTBEAT_TIMEOUT=N : N[1..255] seconds
#
HEARTBEAT_TIMEOUT=0
略)

編集したら OS を再起動して /etc/default/sleepi3 の変更を反映させます。
これでウォッチドッグタイマが無効化されます。

おわりに

slee-Pi 3 の外部入力と外部出力を使用してフェンシングを行う方法を紹介しました。
コンテナなどのサービスの安定運用が可能になるので参考にしていただけると幸いです。

  1. STONITH とは Shoot-The-Other-Node-In-The-Head の略です。問題のあるノードや同時アクセスによるデータ破損を防止します。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?