如何简单地监视网站状态

为了远程监视服务器的性能,专业人员使用特殊的软件系统,例如ZabbixIcinga 。 但是,如果您是负载较小的一个或两个网站的新手所有者或管理员,则无需大型监视系统。 主要参数是网站是否快速为用户服务。 因此,您可以使用简单的程序从连接到Internet的任何计算机上监视站点的工作。

摄影:Mikhail Vasiliev,Unsplash.Com

现在,让我们编写此脚本-最简单的网站可用性和速度监控。 该程序可以在家用计算机,智能手机等上运行。 该程序只有两个功能:

  • 在屏幕上显示您的网站为用户提供页面的时间,
  • 如果站点响应缓慢或出现错误,则程序会将数据写入文件(“日志”或日志文件)。 这些数据值得不时检查以解决刚开始的问题。 因此,我们将以清晰,方便的形式记录这些日志,以便快速查看。

我将详细描述每个步骤,以便即使是只对编写批处理文件(在DOS和Windows上为bat和cmd,在UNIX之类的系统上为sh)有点熟悉的初学者也可以毫无问题地找到答案,并能够使脚本适应他的需要。 但是,我请您不要过分使用该脚本,因为如果使用不正确,它可能不会给出正确的结果,并且还会吞噬大量流量

我将描述Linux等操作系统的脚本及其在家用计算机上的使用。 按照相同的原理,这可以在其他平台上完成。 对于那些只考虑Linux可能性的人来说,另一个有趣的例子可能是它的脚本是一种简单而强大的工具。

1.组织第一


我们将为此程序创建一个单独的文件夹,并在其中创建3个文件。 例如,我有这个文件夹/ home / me / Progs / iNet / monitor(这里我是我的用户名,Progs / iNet是我的与Internet相关的程序的文件夹,monitor是该程序的名称,来自Monitor一词。因为我是这台计算机的唯一用户,所以我将这些文件保存在个人文件夹(/ home)中的磁盘的单独部分中,以便在重新安装系统时将其保存。 在此文件夹中将包含文件:

  • README.txt-这是描述(如果是硬化症):哪种程序,其背景信息等。
  • mon.sh-将有一个程序对该站点进行轮询。
  • server.log-在这里它将记录站点状态指示器。 就我们而言,这只是网站响应的日期,时间和持续时间(如果服务器对我们的请求有错误响应,则还包括其他信息)。

(为了方便编辑和还原文件,可以在Git版本控制系统中包含此文件夹;在此不再赘述)。

2.坚持和放松


我们将以固定的小间隔(例如60秒)运行mon.sh文件。 我使用了操作系统提供的标准工具(更确切地说是桌面环境)。

我的桌面环境是Xfce。 这是Linux上最受欢迎的选项之一。 我喜欢Xfce,因为它使您几乎可以完全自定义整个环境-随心所欲。 同时,Xfce比其他两个知名系统Gnome和KDE更为紧凑和快速。 (其他选项也很有趣-例如,LXDE环境比Xfce更快,更容易,但到目前为止还没有那么丰富的功能)。

对于Xfce,我们需要的工具是Generic Monitor工作面板的插件。 通常它已经安装了,但是如果没有安装,安装程序会很容易找到它:“ Genmon”(描述: xfce4-genmon-plugin )。 这是一个可以添加到面板上并在以下设置中设置的插件:(1)要执行的命令和(2)执行频率(以秒为单位)。 就我而言,命令:

/home/me/Progs/iNet/monitor/mon.sh

(或等效地,〜/ Progs / iNet / monitor / mon.sh)。

Genmon

3.当程序还没有出现时


如果您现在已完成所有这些步骤-创建文件并在面板中启动了插件-那么您将在此处看到启动我们的程序的结果(错误消息:mon.sh文件不是程序)。 然后,要使文件可执行,请转到我们的文件夹并设置其运行权限-
  • 或文件管理器:属性-权限-允许该文件作为程序运行;
  • 或来自终端的命令: chmod 755 mon.sh

在文件本身中,我们编写第一行:

#!/bin/bash # Monitor server responses (run this every 60 seconds): # info=$(curl -I -o /dev/stdout -w '%{time_total}' --url https://example.ru/ -m 9 -s) errr=$(echo $?) 

代替您的站点名称,而不是“ example.ru”,您将观察其状态。 如果它适用于http,请使用http而不是https。 #!/ Bin / bash行表示这是由Bash程序启动的脚本( bash可能是在Linux上执行脚本最常见的脚本)。 要在另一个外壳中工作,请使用它代替Bash。 其余的开头带有尖线的行是注释。 代码本身的第一行是info = $() ,在这些括号中是带有参数的curl命令。 这样的构造-something = $(something)-表示“在括号中执行命令并将结果分配给左侧的变量”。 在这种情况下,我将变量命名为“ info”。 括号中的命令-curl(用某些术语称为“ Kurl”,用行话)将请求发送到指定地址的网络,并将服务器响应返回给我们。 (我的熟人说,他解释说:“起重机如何发出咕gr声,并从空中其他起重机那里收到回应。”) 考虑以下选项:

 curl -I -o /dev/stdout -w '%{time_total}' --url https://example.ru/ -m 9 -s 

-我的意思是我们不请求整个页面,而仅请求其“ HTTP标头”。 每次我们都不需要整个页面的文本来确保网站正常运行时。 由于我们将经常检查该站点,因此,使传输数据的大小尽可能小很重要。 这样既节省了流量,电力,又节省了服务器的负载以及野生生物。

顺便说一下,请注意主机上有多少空闲流量(每月不使用)。 在下面,您将看到传输了多少数据,并且可以计算是否有足够的储备; 如果有的话,现场验证时间可以增加到5、10甚至30-60分钟。 尽管在这种情况下,图片不会很完整-可能不会引起轻微的打扰; 但是,通常,在监视站点时,检测长期问题比单个中断更为重要。 另外,您可以在呼叫计算机上承受哪些流量? 就我而言-无限流量的台式机-并不是那么重要。 但对于具有限制的移动设备或资费标准,则值得计算,并且有可能增加检查间隔。

4.步骤与步骤(第一步除外)


让我们更进一步: -o / dev / stdout表示“将Kurl从服务器收到的响应发送到这样的文件。” 在这种情况下,这不是磁盘上的文件,而是/ dev / stdout-标准输出设备。 通常,“标准输出设备”是我们的屏幕,在这里我们可以看到程序的结果。 但是在脚本中,我们经常将这个“标准输出”定向为进一步处理(现在-将其保存到info变量中)。 然后,我们通常将团队的结果发送给变量,或将其传递给链中的下一个团队。 要从命令创建链,请使用显示为竖线(“ |”)的管道运算符(“管道”)。

-w'%{time_total} eyayu' :此处-w表示“格式化并将此类附加信息提供给标准输出设备”。 具体来说,我们对time_total感兴趣-我们和服务器之间的请求-响应传输花费了多少时间。 您可能知道,有一个更简单的命令ping,可以快速请求服务器并从中获得响应,pong。 但是Ping仅检查托管服务器是否处于活动状态,并且信号来回往返如此长时间。 这显示了最大的访问速度,但仍然没有告诉我们有关网站产生真实内容的速度。 Ping可以快速工作,同时,由于高负载或某些内部问题,该站点可能会变慢或根本无法工作。 因此,我们完全使用Kurla并获取服务器显示我们内容的实际时间。

(通过该参数,您可以判断服务器是否可以有效地执行任务,典型的响应时间是否对用户方便。没有问题-例如,我在许多托管站点上的站点随着时间的推移而放慢了速度,而我不得不寻找另一台托管服务器)。

您是否注意到{time_total}之后的奇怪字母“ eyu”? 我将它们添加为一个唯一标签,它可能不会出现在服务器发送给我们的标头中(尽管假设您的程序的用户将是也不会是坏的,并且是通往深渊的道路。请不要这样做!..或者,当您这样做时,至少为您感到羞耻)。 通过这个标签(我希望),我们可以轻松地从info变量中的所有代码行中提取所需的信息。 因此, curl -w'%{time_total} ehayu' (其余参数正确)将为我们提供以下信息:

0.215238eyayu

这是访问该网站所花费的秒数,加上我们的标签。 (除了此参数外,在info变量中,我们主要关注“状态码”-状态码,或者简称为服务器响应码。通常,当服务器发出请求的文件时,代码为“ 200”。找不到页面时) ,这就是著名的404。如果服务器上有错误,则通常是500(有错误)。

404

5.创造力-自我测试之父


curl的其余参数如下:

 --url https://example.ru/ -m 9 -s 

这意味着什么:要求提供这样的地址; 最长响应时间为9秒(您可以设置的更少一些,因为很少有用户会等待站点的响应这么长时间,因此他会很快发现站点无法正常工作)。 “ -s”表示无声,即卷曲不会告诉我们不必要的细节。

顺便说一句,桌面面板上通常没有太多空间,因此,为了调试脚本,最好从终端(使用./mon.sh命令在其文件夹中)运行该脚本。 对于我们的面板插件,我们将在命令启动之间放置较长的时间-例如3600秒。

为了进行调试,我们可以在没有框架括号的情况下运行此curl,并查看其产生的结果。 (由此计算出交通消耗)。 我们将主要看到一组HTTP标头,例如:

 HTTP/1.1 302 Found Server: QRATOR Date: Sun, 11 Feb 2019 08:06:57 GMT Content-Type: text/html; charset=UTF-8 Connection: keep-alive Keep-Alive: timeout=15 X-Powered-By: PHP/7.2.14-1+ubuntu16.04.1+deb.sury.org+1 Location: https://habr.com/ru/ X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=63072000; includeSubDomains; preload X-Cache-Status: HIT 0.033113 

我们在第一行看到“ HTTP / 1.1 302 Found”-这意味着:数据传输协议是“ HTTP / 1.1”,服务器响应代码是“ 302 Found”。 请求其他页面时,可能是代码“ 301已永久移动”等。 在另一个示例中,我的服务器的正常响应是“ HTTP / 2 200”。 如果您的服务器通常以不同的方式响应,请用此答案代替该脚本中的“ HTTP / 2 200”。

如我们所见,最后一行,Kurl给出了在多少秒内我们收到的服务器响应以及分配的标签。

有趣的是:我们可以配置站点,以便它报告我们的请求(并且仅报告请求!)有关其状态的其他信息,例如,在“自制” HTTP标头中。 假设网站处理器的负载是多少,可用内存和磁盘空间有多少,数据库的运行速度有多快。 (当然,为此,站点必须是动态的,也就是说,请求必须由程序处理-使用PHP,Node.JS等。另一种选择是在服务器上使用特殊软件)。 但是也许我们还不需要这些细节。 该脚本的目的仅仅是定期监视站点的整体性能。 如果出现问题-我们已经通过其他方式处理诊断。 因此,现在我们正在编写最简单的脚本,该脚本适用于任何站点,甚至是静态站点,也不需要服务器上的其他设置。 将来,如果需要,可以扩展脚本的功能; 在此基础上,做基础。

字符串errr = $(echo $?)意思是:将“ echo $?”命令的结果写入errr变量。 echo命令的意思是在标准输出设备(stdout)上打印一些文本,以及字符“ $?”。 -这是当前的错误代码(保留在先前的命令中)。 因此,如果我们的curl突然无法到达服务器,它将给出一个非零的错误代码,我们将通过检查errr变量来找出有关错误的代码。

6.权宜之计


在这里,我想稍微偏离技术问题,以使阅读文本更有趣。 (如果您不同意,请跳过三段)。 我将说,所有人类活动都以其自己的方式是适当的。 即使有人故意将额头敲打在墙壁上(或在地板上……在键盘上……),这也有其自己的权宜之计。 例如,它提供了情感能量的出口-可能不是“最佳”方式,而是如何。 (是的,从此时此刻的角度来看,“最佳”这一概念毫无意义,因为只有与其他事物相比,最佳和最坏的情况才有可能)。

我现在正在写这篇文章,而不是看起来对我来说更重要的案例-为什么? 首先,为了盘点一下,回顾并在内部对我这两天在编写脚本时所学到的内容进行分类...将其从RAM中删除并存储。 其次,通过这种方式,我可以稍作休息...第三,我希望这种经过解释的解释对其他人(可能还有我自己)有用。 如何,关于脚本和服务器请求的最佳但独特的提示并不重要。

也许有人会在某些方面纠正我,结果会变得更好。 有人会获得一些有用的信息。 合适吗 也许……这就是我现在可以做的,现在正在做。 明天,也许我会醒来并重新评估当前的行动……但是这种重新评估也将为以后的生活提供有益的帮助。

因此,完成请求后,我们有两个数据:

  • 来自服务器的主要信息块(当然,如果服务器响应来了),我们将其写入info变量中;
  • curl命令错误代码(如果没有错误,则为0)-写入errr变量。

正是由于同时需要两个总数(包括信息和错误代码),我将它们写入变量中,并且没有立即通过管道将Kurl的总数传递给其他团队。 但是现在在此脚本中,是时候爬上管道了:

 code=$(echo "$info" | grep HTTP | grep -v 'HTTP/2 200') date=$(echo "$info" | grep -i 'date:') dlay=$(echo "$info" | grep  | sed -e 's///') 

在每一行中,我们编写另一个变量-保存命令执行的结果。 在每种情况下,它都不是一个团队,而是连锁。 第一个链接到处都是相同的: echo“ $ info” -将我们保存的,从curl接收到的信息块返回到标准输出设备(stdout)的流。 在管道的更下方,该流被传递到grep命令。 Grep从所有行中仅选择与模式匹配的行。 (-i选项表示“不区分大小写”)。 如您所见,在第一种情况下,我们选择包含“ HTTP”的行。 从其余的堆中拉出的这条线沿管道向下传递到命令grep -v'HTTP / 2 200' 。 在这里, -v选项-e选项相反,它过滤掉找到这种模式的行。 总计,在代码变量中,服务器响应代码(例如,找到的“ HTTP / 1.1 302”)将占一行,但前提是它不是“ HTTP / 2 200”。 因此,在正常情况下,我会过滤掉我的站点发送的行,并仅保存“异常”答案。 (如果您的服务器响应正常,请在此替换它)。

同样,在可变日期中,我们编写了服务器发布其当前日期和时间的行。 这类似于“ date:Sun,11 Feb 2019 08:06:57 GMT ”(通常在格林尼治标准时间(GMT)时区,也称为UTC)。 我们需要在日志文件(“服务器状态日志”)-server.log中写入日期(但每天仅一次!)。 同时,我们将在屏幕上显示该时间。 您可以从计算机上将日期时间写入日志,但是为了方便起见,我们采用了这些时间,因为服务器无论如何都会发送它们。

它与我们的第三个变量dlay(来自英语单词delay-delay)类似。 我们在招标标签“ eeyu”中选择该行,在该行中,我们等待服务器响应的持续时间。 我们使用sed命令删除不再需要的标签,并保存结果。

现在,如果我们打印这些变量以进行验证-例如,将行添加到脚本中:

 printf 'errr: %s\n' "$errr" printf 'code: %s\n' "$code" printf '%s\n' "$date" printf 'dlay: %s\n' "$dlay" 

您应该得到类似以下内容:

 errr: 0 code: date: Mon, 11 Feb 2019 12:46:18 GMT dlay: 0.236549 

总计:curl的错误代码为零(表示正常工作)。 服务器状态代码-未记录(正常)。 日期和时间。 响应的持续时间。 它仍然可以在屏幕上正确显示所需的内容,并在必要时写入文件。

这是最有趣的:什么,在什么条件下以及如何记录?

7.棘手的结论


为了不让您感到更多细节,我将简要地告诉您(Internet上所有这些命令上都有足够的好目录 )。 顺便说一下,简洁是我们在写入此日志时将要实现的主要目标之一。 这样便可以方便查看,并且永远不会占用过多的磁盘空间(与每天以兆字节增长的其他日志不同)。

第一:我们确保只将日期写入日志一次,而不是每行一次。 为此,请从可变日期中分别选择当前日期(curr)和时间(time):

 curr=$(echo "$date" | sed -e 's/\(20[0-9][0-9]\).*$/\1/') time=$(echo "$date" | sed -e 's/^.*\ \([0-9][0-9]:.*\)\ GMT\r$/\1/') 

此外,我们还考虑了日志文件中带有日期的行,并采用了最后一个日期(上一个):

 prev=$(cat /home/me/Progs/iNet/monitor/site.log | grep -e 'date:' | tail -1) 

如果我们当前的日期(当前)不等于前一个日期(来自文件prev),则意味着新的一天到来了(或者说日志文件为空); 然后将新日期写入文件:

 if [[ $curr != $prev ]]; then printf '%s\n' "$curr" >>/home/me/Progs/iNet/monitor/site.log printf '%s %s %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log 

...并同时记录当前信息:时间,从站点接收响应的延迟,站点响应代码(如果不是普通的):

  printf '%s %s %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log 

这将有助于导航:某某日子以某某站点的速度开始。 在其他情况下,我们不要将不必要的条目弄乱文件。 当然,如果收到异常的服务器状态代码,我们将其记录下来:

 elif [[ -n $code ]]; then printf '%s %s %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log 

如果服务器响应时间比平时长,也要写。 我的网站通常负责0.23-0.25秒,因此我记录了超过0.3秒的答案:

 elif (( $(echo "$dlay > 0.3" | bc -l) )); then printf '%s %s\n' "$time" "$dlay" >>/home/me/Progs/iNet/monitor/site.log 

最后,每小时一次,我只需记录从服务器接收的时间-表示它还处于活动状态,并同时作为文件的标记:

 else echo "$time" | grep -e :00: | cat >>/home/me/Progs/iNet/monitor/site.log fi 

我们获得文件的内容,其中带有每小时记录的标记在视觉上(无需通读)可以帮助您查看负载的更高或更低(每小时更多的记录):

 19:42:28 0.461214 19:53:29 0.443956 20:00:29 20:09:30 2.156462 20:10:29 0.358294 20:45:29 0.313378 20:51:30 0.563886 20:54:30 0.307219 21:00:30 0.722343 21:01:30 0.310284 21:09:30 0.379662 21:10:31 1.305779 21:12:35 5.799455 21:23:31 1.054537 21:24:31 1.230391 21:40:31 0.461266 21:42:37 7.140093 22:00:31 22:12:37 5.724768 22:14:31 0.303500 22:42:37 5.735173 23:00:32 23:10:32 0.318207 date: Mon, 11 Feb 2019 00:00:34 0.235298 00:01:33 0.315093 01:00:34 01:37:41 5.741847 02:00:36 02:48:37 0.343234 02:56:37 0.647698 02:57:38 1.670538 02:58:39 2.327980 02:59:37 0.663547 03:00:37 03:40:38 0.331613 04:00:38 04:11:38 0.217022 04:50:39 0.313566 04:55:45 5.719911 05:00:39 

最后,我们在屏幕上显示信息。 而且,如果curl失败,我们将显示并编写有关它的消息(并同时运行Ping并登录以检查服务器是否还处于活动状态):

 printf '%s\n%s\n%s' "$time" "$dlay" "$code" if (( $errr != 0 )); then date >>/home/me/Progs/iNet/monitor/site.log date printf 'CURL Request failed. Error: %s\n' "$errr" >>/home/me/Progs/iNet/monitor/site.log printf 'CURL Request failed. Error: %s\n' "$errr" pung=$(ping -c 1 178.248.237.68) printf 'Ping: %s\n----\n' "$pung" >>/home/me/Progs/iNet/monitor/site.log printf 'Ping: %s\n' "$pung" fi 

将ping字符串中的IP地址替换为您的站点的真实IP地址。

8.后记


工作结果:

最简单的网站监控器

在面板的左侧,您可以看到UTC时间和站点的当前响应速度。 右边是日志:在几小时的负载时间内,即使快速滚动也可以看到集会。 您还可以注意到异常缓慢的响应(峰值;尽管尚不清楚它们来自何处)。

仅此而已。 事实证明,该脚本很简单,可以进行改进:考虑代理和缓存,以优化,可移植性,改进通知和显示的方式进行工作...

但是已经在此类程序中,它可能可以使您了解站点的状态。 并使其成为明智地使用的站点,对人类和所有生物都有用!

脚本的全文和注释。 不要忘记进行必要的更改!
 #!/bin/bash # Monitor server responses (run this every 60 seconds): info=$(curl -I -o /dev/stdout -w '%{time_total}' --url https://example.ru/ -m 9 -s) errr=$(echo $?) # errr = CURL error code https://curl.haxx.se/libcurl/c/libcurl-errors.html code=$(echo "$info" | grep HTTP | grep -v 'HTTP/2 200') date=$(echo "$info" | grep -i 'date:') dlay=$(echo "$info" | grep  | sed -e 's///') # code = Response code = 200? # => empty, otherwise response code string # # date = from HTTP Header of the server responded, like: # Date: Sun, 10 Feb 2019 05:01:50 GMT # # dlay = Response delay ("time_total") from CURL, like: # 0.25321 #printf 'errr: %s\n' "$errr" #printf 'code: %s\n' "$code" #printf '%s\n' "$date" #printf 'dlay: %s\n' "$dlay" curr=$(echo "$date" | sed -e 's/\(20[0-9][0-9]\).*$/\1/') time=$(echo "$date" | sed -e 's/^.*\ \([0-9][0-9]:.*\)\ GMT\r$/\1/') prev=$(cat /home/me/Progs/iNet/monitor/site.log | grep -e 'date:' | tail -1) # = Previously logged date, like: # date: Sun, 10 Feb 2019 # Day logged before vs day returned by the server; usually the same if [[ $curr != $prev ]]; then # Write date etc., at the beginning of every day: printf '%s\n' "$curr" >>/home/me/Progs/iNet/monitor/site.log printf '%s %s %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log elif [[ -n $code ]]; then # If the response had HTTP error code - log it: printf '%s %s ? %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log elif (( $(echo "$dlay > 0.3" | bc -l) )); then # If the response delay was large - log it: printf '%s %s %s\n' "$time" "$dlay" "$code" >>/home/me/Progs/iNet/monitor/site.log else # If it's the start of an hour - just log the time echo "$time" | grep -e :00: | cat >>/home/me/Progs/iNet/monitor/site.log fi # To screen: printf '%s\n%s\n%s' "$time" "$dlay" "$code" # On CURL error: if (( $errr != 0 )); then date >>/home/me/Progs/iNet/monitor/site.log date printf 'CURL Request failed. Error: %s\n' "$errr" >>/home/me/Progs/iNet/monitor/site.log printf 'CURL Request failed. Error: %s\n' "$errr" pung=$(ping -c 1 178.248.237.68) printf 'Ping: %s\n----\n' "$pung" >>/home/me/Progs/iNet/monitor/site.log printf 'Ping: %s\n' "$pung" fi 



PS。 讨论之后 (于02/12/2019):

正如我希望的那样,专家们写了很多有趣的评论。

经过深思熟虑,我可以回答rsashka问题,此脚本的优点是什么。

其他工具,例如NetData (感谢tchspprt的提示!), 提供了大量的数据,这些数据将不会长期存储。 NetData是您每天工作,专业维护网站的好工具。 非常适合诊断当前的问题。

像我的脚本在做其他事情时要注意。 该脚本不需要特殊的研究和设置。 对于外行人来说,这还不错。 而且它的日志占用的空间非常小,根本无法删除。 他们可以累积多年,在N + 1年中,您会看到:“哇,在2019年,我的响应时间降低了一半半!!。”

也就是说,这样的解决方案有其自己的优势-主要针对非系统管理员。 (正如tchspprt所说:“这是关于如何在度假时喂养邻居的猫”)。

Andreymal建议采用一种有趣的方式来考虑,然后仅通过站点上的访问日志即可查看站​​点的负载,而无需额外的资金。您可以在它们上构建漂亮的图形。我可能会尝试此选项,然后在Github上发布发生了什么情况。

unnforgiven建议了另一个有趣的解决方案-可能是一个简单的解决方案(通过docker composer安装prometheus,blackbox和alermanager)。简单来说,便宜的VPS不足以容纳此内存。和具有旧内核的Linux-Docker无法启动。但是,谢谢您的选择!

另一个提示tchspprt:石墨+普罗米修斯+格拉法纳。或者为脚本提供漂亮的图形(gnuplot或rrdtool)。

Mcalexvrn建议使用一个简单的工具:uptimerobot谢谢你

我感谢大家提供了这么多信息!让它对人们有用...

Source: https://habr.com/ru/post/zh-CN439894/


All Articles